@vellumai/assistant 0.9.0 → 0.10.0-staging.2

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 (572) hide show
  1. package/ARCHITECTURE.md +18 -34
  2. package/bun.lock +7 -8
  3. package/docs/activation-funnel-telemetry.md +28 -22
  4. package/docs/architecture/security.md +29 -28
  5. package/docs/stt-provider-onboarding.md +3 -5
  6. package/docs/workflows-testing.md +13 -44
  7. package/docs/workflows.md +3 -5
  8. package/node_modules/@vellumai/ces-client/src/__tests__/ces-client.test.ts +47 -0
  9. package/node_modules/@vellumai/ces-client/src/rpc-client.ts +28 -5
  10. package/node_modules/@vellumai/environments/src/seeds.ts +2 -5
  11. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +97 -0
  12. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +10 -0
  13. package/node_modules/@vellumai/gateway-client/src/index.ts +32 -6
  14. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +119 -0
  15. package/node_modules/@vellumai/gateway-client/src/types.ts +15 -84
  16. package/openapi.yaml +976 -63
  17. package/package.json +2 -1
  18. package/scripts/sync-llm-catalog.ts +6 -15
  19. package/scripts/sync-web-search-catalog.ts +3 -11
  20. package/src/__tests__/access-request-card-view.test.ts +98 -0
  21. package/src/__tests__/access-request-seed-content-blocks.test.ts +2 -4
  22. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +72 -32
  23. package/src/__tests__/agent-loop-compaction-strip.test.ts +241 -0
  24. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +16 -13
  25. package/src/__tests__/agent-loop-output-hooks.test.ts +69 -0
  26. package/src/__tests__/agent-loop-override-profile.test.ts +25 -0
  27. package/src/__tests__/always-loaded-tools-guard.test.ts +2 -3
  28. package/src/__tests__/app-compiler.test.ts +15 -1
  29. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  30. package/src/__tests__/assistant-feature-flag-guard.test.ts +1 -4
  31. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +0 -2
  32. package/src/__tests__/auth-fallback-events-store.test.ts +6 -14
  33. package/src/__tests__/avatar-identity-sync.test.ts +2 -27
  34. package/src/__tests__/btw-routes.test.ts +6 -8
  35. package/src/__tests__/call-pointer-messages.test.ts +28 -0
  36. package/src/__tests__/cancel-clears-processing.test.ts +89 -0
  37. package/src/__tests__/channel-approval-routes.test.ts +0 -4
  38. package/src/__tests__/channel-inbound-disk-pressure.test.ts +5 -15
  39. package/src/__tests__/checker.test.ts +0 -3
  40. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +3 -4
  41. package/src/__tests__/compactor-image-manifest-trust.test.ts +21 -1
  42. package/src/__tests__/compactor-summary-call-truncation.test.ts +223 -0
  43. package/src/__tests__/config-loader-backfill.test.ts +268 -27
  44. package/src/__tests__/config-schema.test.ts +35 -0
  45. package/src/__tests__/config-watcher.test.ts +0 -18
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +2 -2
  47. package/src/__tests__/contact-store-user-file.test.ts +0 -6
  48. package/src/__tests__/contacts-tools.test.ts +29 -0
  49. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +22 -0
  50. package/src/__tests__/conversation-agent-loop-overflow.test.ts +1 -0
  51. package/src/__tests__/conversation-agent-loop.test.ts +58 -0
  52. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  53. package/src/__tests__/conversation-lifecycle.test.ts +7 -9
  54. package/src/__tests__/conversation-load-history-repair.test.ts +101 -0
  55. package/src/__tests__/conversation-routes-guardian-reply.test.ts +15 -12
  56. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +6 -3
  57. package/src/__tests__/conversation-title-service.test.ts +62 -0
  58. package/src/__tests__/credential-broker.test.ts +449 -1
  59. package/src/__tests__/credential-execution-shell-lockdown.test.ts +18 -11
  60. package/src/__tests__/credential-execution-tools.test.ts +0 -1
  61. package/src/__tests__/credential-prompt-route.test.ts +4 -4
  62. package/src/__tests__/credential-routes.test.ts +360 -0
  63. package/src/__tests__/credential-security-invariants.test.ts +4 -13
  64. package/src/__tests__/disk-pressure-policy.test.ts +12 -0
  65. package/src/__tests__/disk-usage.test.ts +65 -0
  66. package/src/__tests__/dynamic-page-surface.test.ts +152 -1
  67. package/src/__tests__/fixtures/credential-security-fixtures.ts +2 -33
  68. package/src/__tests__/gateway-flag-listener.test.ts +110 -1
  69. package/src/__tests__/gateway-only-guard.test.ts +3 -7
  70. package/src/__tests__/guardian-binding-drift-heal.test.ts +1 -1
  71. package/src/__tests__/guardian-card-withdrawal.test.ts +403 -0
  72. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  73. package/src/__tests__/guardian-grant-minting.test.ts +3 -35
  74. package/src/__tests__/guardian-routing-invariants.test.ts +64 -26
  75. package/src/__tests__/guardian-routing-state.test.ts +0 -1
  76. package/src/__tests__/headless-browser-mode.test.ts +10 -0
  77. package/src/__tests__/headless-browser-navigate.test.ts +8 -3
  78. package/src/__tests__/helpers/create-guardian-binding.ts +0 -1
  79. package/src/__tests__/host-browser-proxy.test.ts +87 -0
  80. package/src/__tests__/identity-routes.test.ts +0 -189
  81. package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
  82. package/src/__tests__/injector-v3-suppression.test.ts +27 -20
  83. package/src/__tests__/internal-telemetry-routes.test.ts +6 -14
  84. package/src/__tests__/invite-redemption-service.test.ts +4 -7
  85. package/src/__tests__/llm-callsite-catalog.test.ts +5 -6
  86. package/src/__tests__/llm-catalog-parity.test.ts +30 -23
  87. package/src/__tests__/llm-resolver.test.ts +70 -24
  88. package/src/__tests__/llm-schema.test.ts +1 -0
  89. package/src/__tests__/managed-profile-guard.test.ts +163 -4
  90. package/src/__tests__/mcp-health-check.test.ts +6 -7
  91. package/src/__tests__/media-stream-server-integration.test.ts +317 -13
  92. package/src/__tests__/oauth-provider-seed-logos.test.ts +4 -6
  93. package/src/__tests__/onboarding-persona-write.test.ts +1 -1
  94. package/src/__tests__/path-policy.test.ts +34 -0
  95. package/src/__tests__/persona-resolver.test.ts +49 -14
  96. package/src/__tests__/plugin-api-model-profiles.test.ts +178 -0
  97. package/src/__tests__/plugin-api-provider.test.ts +24 -0
  98. package/src/__tests__/plugin-tool-contribution.test.ts +6 -3
  99. package/src/__tests__/post-compaction-reinjection-idempotency.test.ts +214 -0
  100. package/src/__tests__/provider-send-message-override-profile.test.ts +76 -0
  101. package/src/__tests__/reaction-persistence.test.ts +150 -29
  102. package/src/__tests__/registry.test.ts +2 -7
  103. package/src/__tests__/relay-server.test.ts +285 -0
  104. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -1
  105. package/src/__tests__/schedule-routes-workflow-validation.test.ts +1 -10
  106. package/src/__tests__/schedule-routes.test.ts +0 -30
  107. package/src/__tests__/schedule-tools.test.ts +2 -18
  108. package/src/__tests__/scheduler-reuse-conversation.test.ts +8 -5
  109. package/src/__tests__/skill-execute-input.test.ts +51 -1
  110. package/src/__tests__/skill-runtime-path.test.ts +2 -3
  111. package/src/__tests__/skills.test.ts +51 -0
  112. package/src/__tests__/slack-notification-approval-card.test.ts +176 -0
  113. package/src/__tests__/slack-reaction-canonical-approval.test.ts +285 -0
  114. package/src/__tests__/subagent-tools.test.ts +266 -0
  115. package/src/__tests__/surface-completion-nudge-hook.test.ts +367 -0
  116. package/src/__tests__/task-progress-nudge-hook.test.ts +1 -1
  117. package/src/__tests__/title-generate-hook.test.ts +100 -3
  118. package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -29
  119. package/src/__tests__/token-manager.test.ts +519 -0
  120. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +1 -1
  121. package/src/__tests__/tool-audit-listener.test.ts +7 -7
  122. package/src/__tests__/tool-executor-lifecycle-events.test.ts +6 -3
  123. package/src/__tests__/tool-executor.test.ts +0 -79
  124. package/src/__tests__/trusted-contact-approval-notifier.test.ts +4 -2
  125. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +220 -3
  126. package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
  127. package/src/__tests__/trusted-contact-verification.test.ts +8 -10
  128. package/src/__tests__/twilio-routes.test.ts +81 -1
  129. package/src/__tests__/voice-invite-redemption.test.ts +2 -3
  130. package/src/__tests__/weak-open-model.test.ts +30 -0
  131. package/src/__tests__/web-search-catalog-parity.test.ts +6 -25
  132. package/src/__tests__/workspace-greetings.test.ts +152 -0
  133. package/src/__tests__/workspace-migration-105-enable-memory-v3-live-for-new-workspaces.test.ts +149 -0
  134. package/src/__tests__/workspace-migration-108-drop-balanced-economy-profile.test.ts +285 -0
  135. package/src/__tests__/workspace-migration-add-send-diagnostics.test.ts +1 -1
  136. package/src/__tests__/workspace-migration-drop-collect-usage-data.test.ts +118 -0
  137. package/src/__tests__/workspace-migration-drop-send-diagnostics.test.ts +118 -0
  138. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +0 -4
  139. package/src/agent/loop.ts +49 -29
  140. package/src/api/README.md +6 -6
  141. package/src/api/events/tool-result.ts +6 -0
  142. package/src/api/events/workflow-completed.ts +53 -0
  143. package/src/api/events/workflow-leaf-finished.ts +38 -0
  144. package/src/api/events/workflow-leaf-started.ts +35 -0
  145. package/src/api/events/workflow-progress.ts +32 -0
  146. package/src/api/events/workflow-started.ts +31 -0
  147. package/src/api/index.ts +40 -0
  148. package/src/api/responses/conversation-message.ts +28 -4
  149. package/src/api/responses/home.ts +26 -4
  150. package/src/api/responses/workflow-journal.ts +53 -0
  151. package/src/approvals/guardian-card-withdrawal.ts +145 -0
  152. package/src/approvals/guardian-decision-primitive.ts +26 -3
  153. package/src/approvals/guardian-request-resolvers.ts +183 -80
  154. package/src/calls/__tests__/channel-admission-reader.test.ts +132 -0
  155. package/src/calls/__tests__/relay-setup-router.test.ts +350 -0
  156. package/src/calls/call-pointer-messages.ts +10 -4
  157. package/src/calls/channel-admission-reader.ts +104 -0
  158. package/src/calls/guardian-dispatch.ts +17 -45
  159. package/src/calls/media-stream-server.ts +84 -2
  160. package/src/calls/relay-access-wait.ts +1 -1
  161. package/src/calls/relay-server.ts +66 -0
  162. package/src/calls/relay-setup-router.ts +82 -1
  163. package/src/calls/twilio-routes.ts +17 -8
  164. package/src/calls/voice-session-bridge.ts +2 -2
  165. package/src/cli/commands/clients.ts +3 -0
  166. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2-compare-render.test.ts +1 -1
  167. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v2.test.ts +8 -7
  168. package/src/cli/commands/{__tests__ → memory/__tests__}/memory-v3.test.ts +5 -4
  169. package/src/cli/commands/memory/index.ts +30 -0
  170. package/src/cli/commands/{memory-v2-compare-render.ts → memory/memory-v2-compare-render.ts} +1 -1
  171. package/src/cli/commands/{memory-v2.ts → memory/memory-v2.ts} +6 -15
  172. package/src/cli/commands/{memory-v3.ts → memory/memory-v3.ts} +97 -11
  173. package/src/cli/commands/oauth/status.test.ts +36 -0
  174. package/src/cli/commands/oauth/status.ts +23 -3
  175. package/src/cli/commands/plugins.ts +197 -4
  176. package/src/cli/lib/__tests__/diff-plugin.test.ts +443 -0
  177. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  178. package/src/cli/lib/__tests__/merge-plugin-tree.test.ts +443 -0
  179. package/src/cli/lib/__tests__/plugin-surfaces.test.ts +111 -0
  180. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +295 -2
  181. package/src/cli/lib/diff-plugin.ts +346 -0
  182. package/src/cli/lib/inspect-plugin.ts +12 -1
  183. package/src/cli/lib/install-from-github.ts +105 -17
  184. package/src/cli/lib/merge-plugin-tree.ts +328 -0
  185. package/src/cli/lib/plugin-fingerprint.ts +14 -0
  186. package/src/cli/lib/plugin-surfaces.ts +104 -0
  187. package/src/cli/lib/upgrade-plugin.ts +298 -10
  188. package/src/cli/program.ts +2 -6
  189. package/src/config/__tests__/sync-gated-profiles.test.ts +368 -0
  190. package/src/config/assistant-feature-flags.ts +22 -7
  191. package/src/config/bundled-skills/contacts/tools/contact-search.ts +0 -1
  192. package/src/config/bundled-skills/messaging/SKILL.md +6 -4
  193. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +9 -8
  194. package/src/config/bundled-skills/subagent/SKILL.md +4 -0
  195. package/src/config/bundled-skills/subagent/TOOLS.json +4 -0
  196. package/src/config/bundled-skills/workflows/SKILL.md +14 -8
  197. package/src/config/bundled-tool-registry.ts +2 -7
  198. package/src/config/call-site-defaults.ts +15 -2
  199. package/src/config/feature-flag-registry.json +46 -31
  200. package/src/config/inference-profile-validation.ts +26 -0
  201. package/src/config/llm-resolver.ts +3 -0
  202. package/src/config/loader.ts +4 -0
  203. package/src/config/memory-v3-gate.ts +11 -0
  204. package/src/config/profile-order.ts +28 -0
  205. package/src/config/schema.ts +8 -6
  206. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  207. package/src/config/schemas/call-site-catalog.ts +7 -0
  208. package/src/config/schemas/channels.ts +11 -0
  209. package/src/config/schemas/elevenlabs.ts +0 -1
  210. package/src/config/schemas/llm.ts +31 -0
  211. package/src/config/schemas/memory-lifecycle.ts +3 -7
  212. package/src/config/schemas/memory-v3.ts +6 -0
  213. package/src/config/schemas/platform.ts +0 -8
  214. package/src/config/schemas/services.ts +18 -0
  215. package/src/config/seed-inference-profiles.ts +109 -44
  216. package/src/config/skills.ts +21 -0
  217. package/src/config/sync-gated-profiles.ts +220 -0
  218. package/src/contacts/contact-store.ts +89 -106
  219. package/src/contacts/contacts-write.ts +5 -22
  220. package/src/contacts/types.ts +0 -1
  221. package/src/context/compactor.ts +88 -54
  222. package/src/context/strip-injections.ts +58 -10
  223. package/src/context/token-estimator.ts +1 -1
  224. package/src/credential-execution/process-manager.ts +55 -14
  225. package/src/credential-execution/prompted-credential.ts +2 -3
  226. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -2
  227. package/src/daemon/config-watcher.ts +0 -4
  228. package/src/daemon/conversation-agent-loop-handlers.ts +2 -0
  229. package/src/daemon/conversation-agent-loop.ts +114 -22
  230. package/src/daemon/conversation-history.ts +1 -1
  231. package/src/daemon/conversation-lifecycle.ts +3 -5
  232. package/src/daemon/conversation-process.ts +13 -5
  233. package/src/daemon/conversation-runtime-assembly.ts +13 -15
  234. package/src/daemon/conversation-slash.ts +2 -23
  235. package/src/daemon/conversation-surfaces.ts +26 -0
  236. package/src/daemon/conversation-tool-setup.ts +27 -14
  237. package/src/daemon/conversation.ts +66 -14
  238. package/src/daemon/disk-pressure-policy.ts +5 -3
  239. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -1
  240. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -1
  241. package/src/daemon/handlers/config-a2a.ts +0 -2
  242. package/src/daemon/handlers/config-channels.ts +15 -16
  243. package/src/daemon/handlers/config-slack-channel.ts +22 -3
  244. package/src/daemon/handlers/conversations.ts +107 -0
  245. package/src/daemon/host-browser-proxy.ts +41 -0
  246. package/src/daemon/lifecycle.ts +55 -27
  247. package/src/daemon/message-provenance.ts +2 -0
  248. package/src/daemon/message-types/contacts.ts +0 -1
  249. package/src/daemon/message-types/conversations.ts +3 -3
  250. package/src/daemon/message-types/sync.ts +0 -1
  251. package/src/daemon/message-types/web-activity.ts +7 -1
  252. package/src/daemon/message-types/workflows.ts +83 -1
  253. package/src/daemon/orphan-reaper.test.ts +0 -19
  254. package/src/daemon/orphan-reaper.ts +2 -24
  255. package/src/daemon/server.ts +0 -10
  256. package/src/daemon/tool-setup-types.ts +4 -0
  257. package/src/daemon/trust-context.ts +1 -1
  258. package/src/events/tool-audit-listener.ts +2 -2
  259. package/src/home/feed-source-enrichment.test.ts +151 -0
  260. package/src/home/feed-source-enrichment.ts +176 -0
  261. package/src/home/relationship-state.ts +2 -4
  262. package/src/instrument.ts +18 -6
  263. package/src/ipc/__tests__/binary-result-ipc.test.ts +81 -0
  264. package/src/ipc/__tests__/clients-list-ipc.test.ts +20 -0
  265. package/src/ipc/assistant-server.ts +37 -4
  266. package/src/ipc/gateway-flag-listener.ts +18 -2
  267. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +5 -16
  268. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +7 -11
  269. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +37 -7
  270. package/src/memory/__tests__/memory-retrospective-job.test.ts +229 -401
  271. package/src/memory/__tests__/onboarding-events-store.test.ts +7 -7
  272. package/src/memory/auth-fallback-events-store.ts +2 -2
  273. package/src/memory/auto-analysis-enqueue.ts +3 -5
  274. package/src/memory/bookmark-crud.ts +1 -2
  275. package/src/memory/canonical-guardian-store.ts +39 -1
  276. package/src/memory/conversation-crud.ts +9 -4
  277. package/src/memory/conversation-key-store.ts +17 -2
  278. package/src/memory/conversation-title-service.ts +64 -7
  279. package/src/memory/db-init.ts +17 -17
  280. package/src/memory/embedding-backend.ts +38 -1
  281. package/src/memory/embedding-billing-breaker.ts +96 -0
  282. package/src/memory/jobs-store.ts +25 -13
  283. package/src/memory/jobs-worker.ts +54 -1
  284. package/src/memory/lifecycle-events-store.ts +2 -2
  285. package/src/memory/memory-retrospective-constants.ts +4 -4
  286. package/src/memory/memory-retrospective-enqueue.ts +31 -6
  287. package/src/memory/memory-retrospective-job.ts +28 -227
  288. package/src/memory/migrations/129-contact-channels-access-fields.ts +18 -9
  289. package/src/memory/migrations/131-drop-legacy-member-guardian-tables.ts +14 -2
  290. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +10 -0
  291. package/src/memory/migrations/291-contact-channels-renormalize-addresses.ts +72 -0
  292. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.test.ts +67 -0
  293. package/src/memory/migrations/292-schedule-default-no-reuse-conversation.ts +25 -0
  294. package/src/memory/migrations/293-workflow-journal-leaf-tokens.ts +32 -0
  295. package/src/memory/migrations/294-drop-external-user-id.ts +31 -0
  296. package/src/memory/migrations/295-drop-approval-prompt-ts-tracker.ts +20 -0
  297. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.test.ts +110 -0
  298. package/src/memory/migrations/296-rewrite-balanced-economy-profile-pins.ts +68 -0
  299. package/src/memory/migrations/__tests__/131-drop-legacy-member-guardian-tables.test.ts +154 -0
  300. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +31 -0
  301. package/src/memory/migrations/__tests__/291-contact-channels-renormalize-addresses.test.ts +341 -0
  302. package/src/memory/migrations/__tests__/run-migrations.test.ts +52 -0
  303. package/src/memory/migrations/index.ts +6 -0
  304. package/src/memory/migrations/run-migrations.ts +41 -0
  305. package/src/memory/migrations/validate-migration-state.ts +1 -1
  306. package/src/memory/onboarding-events-store.ts +3 -3
  307. package/src/memory/schema/contacts.ts +0 -5
  308. package/src/memory/skill-loaded-events-store.test.ts +7 -15
  309. package/src/memory/skill-loaded-events-store.ts +2 -2
  310. package/src/memory/tool-executed-events-store.test.ts +7 -7
  311. package/src/memory/turn-trace-store.test.ts +736 -0
  312. package/src/memory/turn-trace-store.ts +364 -0
  313. package/src/memory/v2/__tests__/consolidation-job.test.ts +8 -0
  314. package/src/memory/v2/__tests__/skill-content.test.ts +30 -0
  315. package/src/memory/v2/consolidation-job.ts +2 -2
  316. package/src/memory/v2/skill-content.ts +25 -7
  317. package/src/memory/v2/skill-store.ts +7 -1
  318. package/src/memory/v3-eval/__tests__/eval-packets.test.ts +248 -0
  319. package/src/memory/v3-eval/eval-packets.ts +546 -0
  320. package/src/messaging/providers/slack/adapter.ts +1 -1
  321. package/src/messaging/providers/slack/api.ts +31 -0
  322. package/src/messaging/providers/slack/send.test.ts +114 -2
  323. package/src/messaging/providers/slack/send.ts +30 -7
  324. package/src/messaging/providers/slack/withdraw.test.ts +200 -0
  325. package/src/messaging/providers/slack/withdraw.ts +161 -0
  326. package/src/notifications/AGENTS.md +2 -0
  327. package/src/notifications/access-request-copy.ts +72 -59
  328. package/src/notifications/adapters/shared.ts +29 -0
  329. package/src/notifications/adapters/slack.ts +58 -103
  330. package/src/notifications/adapters/telegram.ts +2 -20
  331. package/src/notifications/approval-card-data.ts +333 -0
  332. package/src/notifications/broadcaster.ts +16 -3
  333. package/src/notifications/canonical-delivery-recorder.ts +139 -0
  334. package/src/notifications/copy-composer.ts +3 -3
  335. package/src/notifications/decision-engine.ts +4 -2
  336. package/src/notifications/destination-resolver.ts +4 -6
  337. package/src/notifications/guardian-question-mode.ts +10 -0
  338. package/src/notifications/home-feed-side-effect.ts +7 -16
  339. package/src/notifications/notification-utils.ts +19 -20
  340. package/src/notifications/signal.ts +79 -43
  341. package/src/notifications/types.ts +98 -121
  342. package/src/oauth/AGENTS.md +5 -24
  343. package/src/permissions/checker.test.ts +51 -0
  344. package/src/permissions/checker.ts +185 -26
  345. package/src/permissions/ipc-risk-types.ts +24 -0
  346. package/src/permissions/question-prompter.test.ts +27 -0
  347. package/src/permissions/question-prompter.ts +4 -0
  348. package/src/platform/client.test.ts +119 -0
  349. package/src/platform/client.ts +66 -0
  350. package/src/platform/consent-cache.test.ts +267 -0
  351. package/src/platform/consent-cache.ts +174 -0
  352. package/src/plugin-api/constants.ts +1 -1
  353. package/src/plugin-api/index.ts +33 -1
  354. package/src/plugin-api/model-profiles.ts +33 -0
  355. package/src/plugin-api/types.ts +50 -2
  356. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +56 -0
  357. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +43 -0
  358. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +137 -0
  359. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +153 -0
  360. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +138 -0
  361. package/src/plugins/defaults/advisor/__tests__/transcript.test.ts +147 -0
  362. package/src/plugins/defaults/advisor/advisor-gate.ts +29 -0
  363. package/src/plugins/defaults/advisor/advisor-state-store.ts +94 -0
  364. package/src/plugins/defaults/advisor/config.ts +21 -0
  365. package/src/plugins/defaults/advisor/consult.ts +93 -0
  366. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +34 -0
  367. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +30 -0
  368. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +19 -0
  369. package/src/plugins/defaults/advisor/package.json +14 -0
  370. package/src/plugins/defaults/advisor/steering.ts +67 -0
  371. package/src/plugins/defaults/advisor/tools/advisor.ts +65 -0
  372. package/src/plugins/defaults/advisor/transcript.ts +76 -0
  373. package/src/plugins/defaults/index.ts +60 -0
  374. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +22 -9
  375. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  376. package/src/plugins/defaults/memory-retrieval/tail-reinjection-strip.ts +64 -0
  377. package/src/plugins/defaults/memory-retrieval/unified-turn-context.ts +29 -21
  378. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1 -0
  379. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  380. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +129 -9
  381. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +31 -4
  382. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +77 -2
  383. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +1 -0
  384. package/src/plugins/defaults/memory-v3-shadow/injector.ts +7 -10
  385. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +144 -11
  386. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +32 -20
  387. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +56 -3
  388. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +23 -2
  389. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +276 -0
  390. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +22 -0
  391. package/src/plugins/defaults/surface-completion-nudge/nudge-state-store.ts +46 -0
  392. package/src/plugins/defaults/surface-completion-nudge/package.json +14 -0
  393. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +3 -13
  394. package/src/plugins/defaults/title-generate/hooks/stop.ts +56 -21
  395. package/src/prompts/persona-resolver.ts +14 -4
  396. package/src/prompts/templates/system-sections.ts +7 -2
  397. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  398. package/src/providers/__tests__/provider-secret-catalog.test.ts +1 -0
  399. package/src/providers/__tests__/retry-callsite.test.ts +176 -0
  400. package/src/providers/atlascloud/client.ts +85 -0
  401. package/src/providers/fetch-provider-catalog.ts +85 -0
  402. package/src/providers/inference/adapter-factory.ts +3 -0
  403. package/src/providers/model-catalog.ts +58 -0
  404. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +33 -0
  405. package/src/providers/openai/chat-completions-provider.ts +7 -0
  406. package/src/providers/openai/responses-provider.ts +10 -0
  407. package/src/providers/provider-send-message.ts +11 -3
  408. package/src/providers/retry.ts +53 -12
  409. package/src/providers/search-provider-catalog.ts +10 -0
  410. package/src/providers/weak-open-model.ts +22 -0
  411. package/src/runtime/AGENTS.md +0 -1
  412. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  413. package/src/runtime/__tests__/client-health.test.ts +44 -0
  414. package/src/runtime/access-request-helper.ts +21 -53
  415. package/src/runtime/actor-trust-resolver.ts +59 -63
  416. package/src/runtime/agent-wake.ts +52 -0
  417. package/src/runtime/assistant-event-hub.ts +18 -4
  418. package/src/runtime/auth/__tests__/route-policy.test.ts +12 -0
  419. package/src/runtime/auth/require-bound-guardian.ts +1 -4
  420. package/src/runtime/btw-sidechain.ts +3 -6
  421. package/src/runtime/capabilities.test.ts +120 -0
  422. package/src/runtime/capabilities.ts +197 -0
  423. package/src/runtime/channel-approval-types.ts +22 -45
  424. package/src/runtime/channel-invite-transports/telegram.ts +4 -4
  425. package/src/runtime/channel-retry-sweep.ts +1 -0
  426. package/src/runtime/channel-verification-service.ts +3 -3
  427. package/src/runtime/client-health.ts +26 -0
  428. package/src/runtime/confirmation-request-guardian-bridge.ts +38 -29
  429. package/src/runtime/effective-capabilities.test.ts +128 -0
  430. package/src/runtime/effective-capabilities.ts +84 -0
  431. package/src/runtime/guardian-reply-router.ts +106 -21
  432. package/src/runtime/invite-redemption-service.ts +9 -25
  433. package/src/runtime/migrations/__tests__/vbundle-builder-fd-leak.test.ts +123 -0
  434. package/src/runtime/migrations/vbundle-builder.ts +49 -20
  435. package/src/runtime/pending-interactions.ts +15 -0
  436. package/src/runtime/routes/__tests__/client-routes.test.ts +13 -0
  437. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +67 -0
  438. package/src/runtime/routes/__tests__/plugins-routes.test.ts +240 -1
  439. package/src/runtime/routes/app-routes.ts +1 -1
  440. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -2
  441. package/src/runtime/routes/assets/vellum-design-system.css +1959 -0
  442. package/src/runtime/routes/browser-tabs-routes.ts +9 -0
  443. package/src/runtime/routes/btw-routes.ts +1 -27
  444. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +17 -8
  445. package/src/runtime/routes/client-routes.ts +10 -0
  446. package/src/runtime/routes/contact-routes.ts +31 -8
  447. package/src/runtime/routes/conversation-compaction-routes.ts +1 -1
  448. package/src/runtime/routes/conversation-management-routes.ts +80 -1
  449. package/src/runtime/routes/conversation-query-routes.ts +68 -22
  450. package/src/runtime/routes/conversation-routes.ts +39 -14
  451. package/src/runtime/routes/credential-routes.ts +40 -16
  452. package/src/runtime/routes/empty-state-greeting-cache.ts +1 -2
  453. package/src/runtime/routes/events-routes.ts +1 -3
  454. package/src/runtime/routes/guardian-approval-interception.ts +14 -73
  455. package/src/runtime/routes/guardian-approval-prompt.ts +22 -4
  456. package/src/runtime/routes/home-feed-routes.ts +8 -3
  457. package/src/runtime/routes/identity-routes.ts +1 -296
  458. package/src/runtime/routes/inbound-message-handler.ts +214 -228
  459. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +89 -7
  460. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +154 -0
  461. package/src/runtime/routes/inbound-stages/admission-policy.ts +140 -0
  462. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +3 -3
  463. package/src/runtime/routes/inbound-stages/background-dispatch.ts +11 -6
  464. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +1 -2
  465. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +1 -2
  466. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +7 -7
  467. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +47 -28
  468. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +358 -0
  469. package/src/runtime/routes/index.ts +2 -0
  470. package/src/runtime/routes/integrations/slack/__tests__/channel.test.ts +8 -0
  471. package/src/runtime/routes/integrations/slack/channel.ts +36 -0
  472. package/src/runtime/routes/internal-telemetry-routes.ts +1 -1
  473. package/src/runtime/routes/mcp-auth-routes.ts +233 -41
  474. package/src/runtime/routes/memory-eval-routes.ts +87 -0
  475. package/src/runtime/routes/notification-routes.ts +122 -133
  476. package/src/runtime/routes/platform-routes.ts +2 -2
  477. package/src/runtime/routes/plugins-routes.ts +202 -3
  478. package/src/runtime/routes/schedule-routes.ts +0 -22
  479. package/src/runtime/routes/secret-routes.ts +10 -0
  480. package/src/runtime/routes/surface-action-routes.ts +2 -1
  481. package/src/runtime/routes/tool-call-question-enrichment.test.ts +146 -0
  482. package/src/runtime/routes/tool-call-question-enrichment.ts +66 -0
  483. package/src/runtime/routes/workflow-routes.test.ts +229 -44
  484. package/src/runtime/routes/workflow-routes.ts +131 -29
  485. package/src/runtime/routes/workspace-greetings.ts +55 -0
  486. package/src/runtime/sync/resource-sync-events.ts +1 -11
  487. package/src/runtime/tool-grant-request-helper.ts +18 -16
  488. package/src/runtime/trust-context-resolver.ts +8 -5
  489. package/src/schedule/inference-profile.ts +2 -14
  490. package/src/schedule/schedule-store.ts +1 -1
  491. package/src/schedule/scheduler-types.ts +5 -1
  492. package/src/security/__tests__/provider-key-env-fallback.test.ts +6 -0
  493. package/src/security/secret-patterns.ts +3 -0
  494. package/src/subagent/manager.ts +17 -4
  495. package/src/subagent/types.ts +6 -0
  496. package/src/telemetry/trace-collection-policy.test.ts +28 -0
  497. package/src/telemetry/trace-collection-policy.ts +30 -0
  498. package/src/telemetry/types.ts +89 -0
  499. package/src/telemetry/usage-telemetry-reporter.test.ts +586 -36
  500. package/src/telemetry/usage-telemetry-reporter.ts +148 -41
  501. package/src/tools/AGENTS.md +3 -3
  502. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +31 -0
  503. package/src/tools/browser/browser-execution.ts +30 -19
  504. package/src/tools/document/document-tool.ts +2 -3
  505. package/src/tools/executor.ts +5 -3
  506. package/src/tools/host-terminal/host-shell.ts +5 -4
  507. package/src/tools/memory/register.ts +2 -2
  508. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +360 -0
  509. package/src/tools/network/__tests__/web-search.test.ts +143 -0
  510. package/src/tools/network/web-fetch.ts +372 -1
  511. package/src/tools/network/web-search-error.ts +1 -1
  512. package/src/tools/network/web-search.ts +213 -10
  513. package/src/tools/permission-checker.ts +4 -3
  514. package/src/tools/registry.ts +20 -0
  515. package/src/tools/schedule/create.ts +7 -12
  516. package/src/tools/schedule/update.ts +4 -11
  517. package/src/tools/shared/filesystem/path-policy.ts +39 -13
  518. package/src/tools/side-effects.ts +2 -17
  519. package/src/tools/skills/execute.ts +33 -0
  520. package/src/tools/subagent/spawn.ts +61 -12
  521. package/src/tools/terminal/shell.ts +10 -4
  522. package/src/tools/tool-approval-handler.ts +18 -13
  523. package/src/tools/tool-manifest.ts +0 -2
  524. package/src/tools/types.ts +9 -0
  525. package/src/tools/ui-surface/definitions.ts +64 -3
  526. package/src/tools/verification-control-plane-policy.ts +3 -1
  527. package/src/tools/workflows/run-workflow.test.ts +8 -18
  528. package/src/tools/workflows/run-workflow.ts +1 -0
  529. package/src/util/disk-usage.ts +78 -23
  530. package/src/util/platform.ts +10 -3
  531. package/src/watcher/telemetry.ts +2 -2
  532. package/src/workflows/capabilities.ts +2 -3
  533. package/src/workflows/engine.test.ts +175 -1
  534. package/src/workflows/engine.ts +82 -0
  535. package/src/workflows/journal-store.test.ts +70 -0
  536. package/src/workflows/journal-store.ts +18 -3
  537. package/src/workflows/run-manager.test.ts +171 -28
  538. package/src/workflows/run-manager.ts +66 -24
  539. package/src/workspace/migrations/105-enable-memory-v3-live-for-new-workspaces.ts +63 -0
  540. package/src/workspace/migrations/106-drop-collect-usage-data.ts +47 -0
  541. package/src/workspace/migrations/107-drop-send-diagnostics.ts +47 -0
  542. package/src/workspace/migrations/108-drop-balanced-economy-profile.ts +129 -0
  543. package/src/workspace/migrations/registry.ts +8 -0
  544. package/src/__tests__/app-control-no-global-cgevent.test.ts +0 -98
  545. package/src/__tests__/credential-security-e2e.test.ts +0 -362
  546. package/src/__tests__/credential-vault-unit.test.ts +0 -1528
  547. package/src/__tests__/credential-vault.test.ts +0 -1706
  548. package/src/__tests__/identity-intro-cache.test.ts +0 -315
  549. package/src/__tests__/secret-onetime-send.test.ts +0 -182
  550. package/src/cli/commands/__tests__/task.test.ts +0 -914
  551. package/src/cli/commands/task.ts +0 -771
  552. package/src/config/bundled-skills/personal-page/SKILL.md +0 -57
  553. package/src/config/bundled-skills/personal-page/TOOLS.json +0 -27
  554. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +0 -17
  555. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +0 -22
  556. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +0 -16
  557. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +0 -77
  558. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +0 -57
  559. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +0 -28
  560. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +0 -234
  561. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +0 -48
  562. package/src/config/preloaded-apps/personal-page/src/components/media.ts +0 -16
  563. package/src/config/preloaded-apps/personal-page/src/index.html +0 -20
  564. package/src/config/preloaded-apps/personal-page/src/main.tsx +0 -7
  565. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +0 -82
  566. package/src/config/preloaded-apps/personal-page/src/styles.css +0 -759
  567. package/src/memory/__tests__/preloaded-apps.test.ts +0 -85
  568. package/src/memory/preloaded-apps.ts +0 -116
  569. package/src/notifications/tool-approval-copy.ts +0 -142
  570. package/src/runtime/routes/approval-prompt-ts-tracker.ts +0 -78
  571. package/src/runtime/routes/identity-intro-cache.ts +0 -172
  572. package/src/tools/credentials/vault.ts +0 -712
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Default `post-model-call` hook: when a user-facing turn is about to end with a
3
+ * progress surface the model showed but never closed, nudge the model — once
4
+ * per run — to complete or dismiss it, then re-query so it can act.
5
+ *
6
+ * Motivation: the model can call `ui_show` with a `task_progress` card (or a
7
+ * `work_result` in an `in_progress` state) to show live progress, but weaker
8
+ * models often never advance it to a terminal status or `ui_dismiss` it. The
9
+ * surface then renders a spinner forever, long after the work finished. Static
10
+ * prompt guidance is easy to ignore; a reminder injected at the moment the turn
11
+ * would otherwise end — with a concrete "you left this open" signal — is far
12
+ * more salient.
13
+ *
14
+ * The nudge is strictly best-effort and self-targeting:
15
+ * - It fires at most once per run (no looping if the model declines).
16
+ * - It is advisory — the model may leave the surface open if the work it
17
+ * represents is genuinely still running.
18
+ * - A model that already completed or dismissed its surfaces is never nudged,
19
+ * so capable models that close their surfaces pay nothing.
20
+ *
21
+ * Only a finalized, no-tool, main-agent reply is actionable:
22
+ * - A provider rejection carries no turn content to assess (a recovery hook
23
+ * like history-repair owns that).
24
+ * - A tool-bearing turn continues naturally — the loop runs the tools and the
25
+ * model gets another chance to close the surface — so we leave it alone.
26
+ * - Background call sites (wake, title-gen, memory) have no live user watching
27
+ * a spinner, so the nudge would only burn a model round.
28
+ *
29
+ * No subagent guard is needed: the `ui-surface` tools are gated on a connected
30
+ * client (see `conversation-tool-setup.ts`), and subagents have none — so a
31
+ * subagent can never create a surface and so can never trigger this hook.
32
+ *
33
+ * The dangling-surface signal is derived from the current response cycle (the
34
+ * messages after the last genuine user prompt) by correlating each progress
35
+ * `ui_show` with its `surface_id` result and folding in later `ui_update` /
36
+ * `ui_dismiss` calls. Deriving the cycle boundary from history content rather
37
+ * than an index means mid-run compaction (which rewrites the array in place)
38
+ * can't invalidate it.
39
+ *
40
+ * The one-shot bound is split across two hooks: this hook marks the
41
+ * conversation when it nudges, and the sibling `stop` hook clears the mark when
42
+ * the turn terminates, so the next run nudges afresh.
43
+ *
44
+ * Defaults register before any user plugin, so this hook runs at the front of
45
+ * the `post-model-call` chain — later hooks see (and may override) its decision.
46
+ */
47
+
48
+ import type { PluginHookFn, PostModelCallContext } from "@vellumai/plugin-api";
49
+
50
+ import type { ContentBlock, Message } from "../../../../providers/types.js";
51
+ import {
52
+ isSurfaceCompletionNudged,
53
+ markSurfaceCompletionNudged,
54
+ } from "../nudge-state-store.js";
55
+
56
+ /**
57
+ * Canonical nudge text. Shown to the model as provider-only context, never to
58
+ * the user. Kept verbatim so a plugin that wraps the default sees a stable
59
+ * string. Deliberately soft: the model may leave the surface open if the work
60
+ * is genuinely still running.
61
+ */
62
+ export const SURFACE_COMPLETION_NUDGE_TEXT =
63
+ '<system_notice>You showed the user a progress surface this turn (a task_progress card or a work_result) and are about to end the turn with it still marked in_progress. If that work is finished, advance it to a terminal state now — call ui_update to set its status to "completed" (or "failed"), or ui_dismiss it — so the user is not left watching a card spin forever. Do this only if the work it represents is actually done; if it is genuinely still running, leave it. Then give your final reply.</system_notice>';
64
+
65
+ /**
66
+ * Surface statuses that mean the progress surface has reached a terminal state
67
+ * and needs no completion nudge. Covers both `task_progress`
68
+ * (`completed`/`failed`) and `work_result` (`completed`/`partial`/`failed`),
69
+ * plus `cancelled` for tolerance.
70
+ */
71
+ const TERMINAL_STATUSES = new Set([
72
+ "completed",
73
+ "failed",
74
+ "partial",
75
+ "cancelled",
76
+ ]);
77
+
78
+ function hasToolUse(content: ReadonlyArray<ContentBlock>): boolean {
79
+ return content.some((block) => block.type === "tool_use");
80
+ }
81
+
82
+ /** A user-role message carrying only tool results, not a fresh prompt. */
83
+ function isToolResultMessage(message: Message): boolean {
84
+ return (
85
+ message.role === "user" &&
86
+ message.content.length > 0 &&
87
+ message.content.every((block) => block.type === "tool_result")
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Messages belonging to the current response cycle: everything after the last
93
+ * genuine user prompt. Falls back to the whole history when none is found.
94
+ */
95
+ function currentCycleMessages(
96
+ messages: ReadonlyArray<Message>,
97
+ ): ReadonlyArray<Message> {
98
+ for (let i = messages.length - 1; i >= 0; i--) {
99
+ const message = messages[i];
100
+ if (message.role === "user" && !isToolResultMessage(message)) {
101
+ return messages.slice(i + 1);
102
+ }
103
+ }
104
+ return messages;
105
+ }
106
+
107
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
108
+ return value !== null && typeof value === "object"
109
+ ? (value as Record<string, unknown>)
110
+ : undefined;
111
+ }
112
+
113
+ /**
114
+ * Pull a surface status out of a `ui_show` / `ui_update` input, tolerating the
115
+ * shapes the server-side normalization accepts: nested under
116
+ * `data.templateData` (task_progress), under `data` (work_result), or at the
117
+ * top level. Returns a lowercased status, or `undefined` when none is present.
118
+ */
119
+ function extractStatus(input: Record<string, unknown>): string | undefined {
120
+ const data = asRecord(input.data);
121
+ const templateData =
122
+ asRecord(data?.templateData) ?? asRecord(input.templateData);
123
+ const raw = templateData?.status ?? data?.status ?? input.status;
124
+ return typeof raw === "string" ? raw.trim().toLowerCase() : undefined;
125
+ }
126
+
127
+ /**
128
+ * Whether a `ui_show` input describes a surface with a progress lifecycle: a
129
+ * `task_progress` card or a `work_result`. Returns the initial status when so.
130
+ */
131
+ function progressShowInfo(input: Record<string, unknown>): {
132
+ isProgress: boolean;
133
+ status: string | undefined;
134
+ } {
135
+ const surfaceType = input.surface_type;
136
+ if (surfaceType === "work_result") {
137
+ return { isProgress: true, status: extractStatus(input) };
138
+ }
139
+ if (surfaceType === "card") {
140
+ const data = asRecord(input.data);
141
+ const template = data?.template ?? input.template;
142
+ if (template === "task_progress") {
143
+ return { isProgress: true, status: extractStatus(input) };
144
+ }
145
+ }
146
+ return { isProgress: false, status: undefined };
147
+ }
148
+
149
+ function surfaceIdOf(input: Record<string, unknown>): string | undefined {
150
+ return typeof input.surface_id === "string" ? input.surface_id : undefined;
151
+ }
152
+
153
+ /** Parse the `{ surfaceId }` JSON a successful `ui_show` returns. */
154
+ function parseSurfaceId(content: string): string | undefined {
155
+ try {
156
+ const parsed = JSON.parse(content) as unknown;
157
+ const record = asRecord(parsed);
158
+ return typeof record?.surfaceId === "string" ? record.surfaceId : undefined;
159
+ } catch {
160
+ return undefined;
161
+ }
162
+ }
163
+
164
+ interface SurfaceState {
165
+ /** Latest known status, lowercased; `undefined` when never set explicitly. */
166
+ status: string | undefined;
167
+ dismissed: boolean;
168
+ }
169
+
170
+ function isNonTerminal(state: SurfaceState): boolean {
171
+ if (state.dismissed) return false;
172
+ return state.status === undefined || !TERMINAL_STATUSES.has(state.status);
173
+ }
174
+
175
+ /**
176
+ * True when the current response cycle left at least one progress surface
177
+ * (a `task_progress` card or `work_result`) open: shown but neither advanced to
178
+ * a terminal status nor dismissed.
179
+ *
180
+ * Each progress `ui_show` is correlated to its `surface_id` via the matching
181
+ * tool result (the result lands in the next message; updates and dismisses
182
+ * arrive in later messages, after the model has the id in hand). Later
183
+ * `ui_update` / `ui_dismiss` calls fold their status / dismissal onto the
184
+ * tracked surface.
185
+ */
186
+ function hasDanglingProgressSurface(messages: ReadonlyArray<Message>): boolean {
187
+ const surfaces = new Map<string, SurfaceState>();
188
+ // tool_use_id -> initial status of a progress ui_show awaiting its result id.
189
+ const pendingShows = new Map<string, string | undefined>();
190
+
191
+ for (const message of currentCycleMessages(messages)) {
192
+ if (message.role === "assistant") {
193
+ for (const block of message.content) {
194
+ if (block.type !== "tool_use") continue;
195
+ if (block.name === "ui_show") {
196
+ const info = progressShowInfo(block.input);
197
+ if (info.isProgress) pendingShows.set(block.id, info.status);
198
+ } else if (block.name === "ui_update") {
199
+ const id = surfaceIdOf(block.input);
200
+ const status = extractStatus(block.input);
201
+ if (id && status !== undefined) {
202
+ const existing = surfaces.get(id);
203
+ if (existing) existing.status = status;
204
+ else surfaces.set(id, { status, dismissed: false });
205
+ }
206
+ } else if (block.name === "ui_dismiss") {
207
+ const id = surfaceIdOf(block.input);
208
+ if (id) {
209
+ const existing = surfaces.get(id);
210
+ if (existing) existing.dismissed = true;
211
+ else surfaces.set(id, { status: undefined, dismissed: true });
212
+ }
213
+ }
214
+ }
215
+ continue;
216
+ }
217
+ if (message.role !== "user") continue;
218
+ for (const block of message.content) {
219
+ // guard:allow-tool-result-only — only the local tool executor's
220
+ // `tool_result` carries a `ui_show` `surfaceId` to correlate. A
221
+ // `web_search_tool_result` comes from a `server_tool_use`, never a
222
+ // `ui_show`, so it can never match a pending show and is correctly skipped.
223
+ if (block.type !== "tool_result") continue;
224
+ if (!pendingShows.has(block.tool_use_id)) continue;
225
+ const initialStatus = pendingShows.get(block.tool_use_id);
226
+ pendingShows.delete(block.tool_use_id);
227
+ const id = parseSurfaceId(block.content);
228
+ if (!id) continue;
229
+ const existing = surfaces.get(id);
230
+ // A later update/dismiss can register the id before its show result is
231
+ // scanned only if history was reordered; guard so we never clobber a
232
+ // known terminal/dismissed state with the initial status.
233
+ if (existing) {
234
+ if (existing.status === undefined && !existing.dismissed) {
235
+ existing.status = initialStatus;
236
+ }
237
+ } else {
238
+ surfaces.set(id, { status: initialStatus, dismissed: false });
239
+ }
240
+ }
241
+ }
242
+
243
+ for (const state of surfaces.values()) {
244
+ if (isNonTerminal(state)) return true;
245
+ }
246
+ return false;
247
+ }
248
+
249
+ const postModelCall: PluginHookFn<PostModelCallContext> = async (ctx) => {
250
+ // A provider rejection carries no turn content to assess (a recovery hook
251
+ // owns the rejection).
252
+ if (ctx.error) return;
253
+ // A tool-bearing turn continues mid-run — the loop runs the tools and the
254
+ // model gets another chance to close the surface — so leave it alone.
255
+ if (hasToolUse(ctx.content)) return;
256
+ // Only nudge the user-facing reply: background call sites have no live user
257
+ // watching a spinner.
258
+ if (ctx.callSite !== "mainAgent") return;
259
+ // One nudge per run; the sibling `stop` hook clears the mark on terminal stop.
260
+ if (isSurfaceCompletionNudged(ctx.conversationId)) return;
261
+
262
+ if (!hasDanglingProgressSurface(ctx.messages)) return;
263
+
264
+ markSurfaceCompletionNudged(ctx.conversationId);
265
+ ctx.messages.push({
266
+ role: "user",
267
+ content: [{ type: "text", text: SURFACE_COMPLETION_NUDGE_TEXT }],
268
+ });
269
+ ctx.decision = "continue";
270
+ ctx.logger.info(
271
+ { plugin: "surface-completion-nudge", conversationId: ctx.conversationId },
272
+ "Turn ending with an open progress surface — nudging the model to complete or dismiss it",
273
+ );
274
+ };
275
+
276
+ export default postModelCall;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Default `stop` hook: clears the per-conversation surface-completion nudge
3
+ * bound when a turn terminates.
4
+ *
5
+ * The `post-model-call` hook (see `./post-model-call.ts`) marks the bound when
6
+ * it nudges the model to close a dangling progress surface. `stop` is the
7
+ * definitive terminal hook — it fires exactly once when the turn is truly
8
+ * ending, after every retry decision has been made — so clearing the bound here
9
+ * unconditionally guarantees the next run nudges afresh, no matter how the turn
10
+ * ended (a finalized reply, an abort, or a retry the loop's per-run backstop
11
+ * refused).
12
+ */
13
+
14
+ import type { PluginHookFn, StopContext } from "@vellumai/plugin-api";
15
+
16
+ import { clearSurfaceCompletionNudged } from "../nudge-state-store.js";
17
+
18
+ const stop: PluginHookFn<StopContext> = async (ctx) => {
19
+ clearSurfaceCompletionNudged(ctx.conversationId);
20
+ };
21
+
22
+ export default stop;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Per-conversation surface-completion nudge state.
3
+ *
4
+ * The `post-model-call` hook nudges the model — once per run — to complete or
5
+ * dismiss a progress surface it left `in_progress` when the turn was about to
6
+ * end, asking the loop to re-query so the model can act on it. That nudge is
7
+ * bounded to one pass per run: if the model declines or fails to close the
8
+ * surface, the hook lets the turn end rather than looping on it.
9
+ *
10
+ * The two hooks split this state's lifecycle: `post-model-call` marks a
11
+ * conversation when it issues the nudge, and the sibling `stop` hook clears the
12
+ * mark when the turn terminates. A conversation therefore only holds an entry
13
+ * while a nudge is in flight, and the next run nudges afresh.
14
+ *
15
+ * This module is side-effect free: importing it only initializes an empty store
16
+ * and registers no plugin.
17
+ */
18
+
19
+ /** Conversations with a surface-completion nudge in flight for the current run. */
20
+ const nudgeInFlight = new Set<string>();
21
+
22
+ /** Whether the conversation already issued its one nudge this run. */
23
+ export function isSurfaceCompletionNudged(conversationId: string): boolean {
24
+ return nudgeInFlight.has(conversationId);
25
+ }
26
+
27
+ /** Record that the conversation issued its one nudge this run. */
28
+ export function markSurfaceCompletionNudged(conversationId: string): void {
29
+ nudgeInFlight.add(conversationId);
30
+ }
31
+
32
+ /**
33
+ * Clear the conversation's nudge mark so the next run nudges afresh. The
34
+ * sibling `stop` hook calls this when the turn terminates.
35
+ */
36
+ export function clearSurfaceCompletionNudged(conversationId: string): void {
37
+ nudgeInFlight.delete(conversationId);
38
+ }
39
+
40
+ /**
41
+ * Test-only: drop every conversation's nudge state so a suite that drives the
42
+ * hook directly starts each case from an empty store.
43
+ */
44
+ export function resetSurfaceCompletionNudgeStoreForTests(): void {
45
+ nudgeInFlight.clear();
46
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "default-surface-completion-nudge",
3
+ "version": "1.0.0",
4
+ "description": "First-party default plugin that nudges the model to complete or dismiss a progress surface it left in_progress when the turn ends.",
5
+ "private": true,
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "engines": {
9
+ "node": ">=20.12.0"
10
+ },
11
+ "peerDependencies": {
12
+ "@vellumai/plugin-api": "^0.8.0"
13
+ }
14
+ }
@@ -48,6 +48,7 @@
48
48
  import type { PluginHookFn, PostToolUseContext } from "@vellumai/plugin-api";
49
49
 
50
50
  import type { ContentBlock, Message } from "../../../../providers/types.js";
51
+ import { isWeakOpenModel } from "../../../../providers/weak-open-model.js";
51
52
 
52
53
  /**
53
54
  * Canonical nudge notice. Module-level constant so tests and wrapping plugins
@@ -56,7 +57,7 @@ import type { ContentBlock, Message } from "../../../../providers/types.js";
56
57
  * are fine and the model may skip it when wrapping up.
57
58
  */
58
59
  export const TASK_PROGRESS_NUDGE_TEXT =
59
- '<system_notice>You are several tool calls into this turn and have not shown the user a progress card. If you are doing multi-step work, call ui_show now with surface_type "card" and template "task_progress" (coarse steps are fine a rough "Working on X" beats no signal) so the user can see what is happening, and keep it updated with ui_update as you go. Skip this if you are about to finish; never let it interrupt the actual work.</system_notice>';
60
+ '<system_notice>You are several tool calls into this turn with no progress card shown. A card is optional, not required: if the turn is wrapping up, is not really multi-step, or you cannot form clean steps, skip it and keep working — a one-line note of what you are doing is a fine substitute, and proceeding with no card is also fine. Only if a live step tracker would genuinely help the user, show it with a SINGLE self-contained ui_show call that already contains the steps: ui_show({ surface_type: "card", data: { template: "task_progress", templateData: { title: "<what you are doing>", status: "in_progress", steps: [{ label: "<step 1>", status: "in_progress" }, { label: "<step 2>", status: "pending" }] } } }). Coarse steps are fine. Do not call ui_show with an empty `data: {}` and fill it in afterward an empty card renders as a blank box; either include the steps now or skip the card. Advance it later with ui_update under `data.templateData`. Never let the card interrupt the actual work; if one ever looks wrong, just dismiss it and move on. You will not be nudged about this again this turn.</system_notice>';
60
61
 
61
62
  /**
62
63
  * Number of tool-use rounds in a turn, with no task_progress card shown, that
@@ -66,17 +67,6 @@ export const TASK_PROGRESS_NUDGE_TEXT =
66
67
  */
67
68
  export const TASK_PROGRESS_NUDGE_ROUND_THRESHOLD = 3;
68
69
 
69
- /**
70
- * Weaker open models that disregard the static progress-card instruction and
71
- * so get the mid-turn nudge: Kimi, DeepSeek, and MiniMax. Family-level matching
72
- * spans provider naming conventions (OpenRouter `moonshotai/kimi-k2.6`,
73
- * `deepseek/deepseek-chat`, `minimax/minimax-m3`; Fireworks
74
- * `accounts/fireworks/models/minimax-m3`, `kimi-k2p6`). Extend as other models
75
- * show the same gap. Capable models (Claude, GPT) follow the prompt and are
76
- * intentionally excluded.
77
- */
78
- const WEAK_MODEL_PATTERN = /kimi|deepseek|minimax/i;
79
-
80
70
  /**
81
71
  * Round count at the last nudge, per conversation. A non-zero entry means the
82
72
  * turn has already been nudged; it resets when the round count drops below the
@@ -145,7 +135,7 @@ function scanTurn(messages: ReadonlyArray<Message>): {
145
135
  }
146
136
 
147
137
  const postToolUse: PluginHookFn<PostToolUseContext> = async (ctx) => {
148
- if (!WEAK_MODEL_PATTERN.test(ctx.model)) return;
138
+ if (!isWeakOpenModel(ctx.model)) return;
149
139
 
150
140
  const { rounds, taskProgressShown } = scanTurn(ctx.messages);
151
141
 
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Default `stop` hook: triggers the second-pass conversation-title
3
- * regeneration once a conversation has accumulated enough context.
2
+ * Default `stop` hook: triggers conversation-title retry/regeneration once a
3
+ * conversation has accumulated useful context.
4
4
  *
5
5
  * The first title is generated from the opening prompt alone (see
6
6
  * `./user-prompt-submit.ts`). After a few exchanges the conversation's real
7
7
  * topic is usually clearer, so a single second pass re-titles using the most
8
- * recent messages. This hook is the trigger — it fires the regeneration when
9
- * the conversation reaches its third user turn and delegates the title
10
- * itself to the service (`memory/conversation-title-service.ts`), which
11
- * re-checks that the title is still auto-generated, resolves the title
12
- * provider, persists, and broadcasts the `conversation_title_updated` /
13
- * `sync_changed` events.
8
+ * recent messages. This hook is the trigger — it retries placeholder titles
9
+ * after successful turns and fires the second-pass regeneration when the
10
+ * conversation reaches its third user turn. The service
11
+ * (`memory/conversation-title-service.ts`) re-checks that the title is still
12
+ * auto-generated, resolves the title provider, persists, and broadcasts the
13
+ * `conversation_title_updated` / `sync_changed` events.
14
14
  *
15
15
  * Turn count is read from history rather than an external counter: the number
16
16
  * of genuine user prompts — user-role messages that aren't purely tool results
@@ -22,7 +22,11 @@ import type { PluginHookFn, StopContext } from "@vellumai/plugin-api";
22
22
 
23
23
  import { getConfig } from "../../../../config/loader.js";
24
24
  import { getConversation } from "../../../../memory/conversation-crud.js";
25
- import { queueRegenerateConversationTitle } from "../../../../memory/conversation-title-service.js";
25
+ import {
26
+ AUTO_TITLE_DETERMINISTIC,
27
+ isReplaceableTitle,
28
+ queueRegenerateConversationTitle,
29
+ } from "../../../../memory/conversation-title-service.js";
26
30
  import type { Message } from "../../../../providers/types.js";
27
31
 
28
32
  /**
@@ -50,37 +54,68 @@ function countUserTurns(messages: ReadonlyArray<Message>): number {
50
54
  return turns;
51
55
  }
52
56
 
57
+ function shouldRetryFallbackTitle(conversation: {
58
+ title: string | null;
59
+ isAutoTitle: number;
60
+ }): boolean {
61
+ if (!conversation.isAutoTitle) return false;
62
+ return (
63
+ isReplaceableTitle(conversation.title) ||
64
+ conversation.isAutoTitle === AUTO_TITLE_DETERMINISTIC
65
+ );
66
+ }
67
+
53
68
  const stop: PluginHookFn<StopContext> = async (ctx) => {
54
69
  // Re-title only at a genuine successful turn end (the model returned a reply
55
70
  // with no tool calls). Any other terminal — a provider rejection, abort, or
56
71
  // an output-limit cutoff — produced no new topic to re-title from.
57
72
  if (ctx.exitReason !== "no_tool_calls") return;
58
73
 
59
- if (getConfig().conversations.skipAutoRetitling) return;
60
-
61
- if (countUserTurns(ctx.messages) !== SECOND_PASS_USER_TURN) return;
74
+ const userTurns = countUserTurns(ctx.messages);
75
+ if (userTurns === 0) return;
62
76
 
63
77
  // System conversations (background/scheduled) keep their deterministic
64
78
  // bootstrap title — multi-prompt background jobs can reach three user-role
65
79
  // turns with no human present, and a refined LLM title isn't worth the
66
80
  // tokens there. The lookup fails open: on a read error the hook behaves as
67
81
  // before (queues regeneration; the service re-checks isAutoTitle).
82
+ let retryFallbackTitle = false;
68
83
  try {
69
84
  const conversation = getConversation(ctx.conversationId);
70
85
  if (conversation && conversation.conversationType !== "standard") return;
86
+ retryFallbackTitle = conversation
87
+ ? shouldRetryFallbackTitle(conversation)
88
+ : false;
71
89
  } catch {
72
90
  // Fall through to queueing.
73
91
  }
74
92
 
75
- const { conversationId } = ctx;
76
- // Deferred to a later macrotask so the just-completed turn's persistence
77
- // settles first. The service regenerates from the most recent stored
78
- // messages, so it must run after the reply is persisted to reflect it. The
79
- // service is itself fire-and-forget and re-checks replaceability, owning
80
- // provider resolution, persistence, and the resulting broadcast.
81
- setTimeout(() => {
82
- queueRegenerateConversationTitle({ conversationId });
83
- }, 0);
93
+ if (
94
+ userTurns === SECOND_PASS_USER_TURN &&
95
+ !getConfig().conversations.skipAutoRetitling
96
+ ) {
97
+ const { conversationId } = ctx;
98
+ // Deferred to a later macrotask so the just-completed turn's persistence
99
+ // settles first. The service regenerates from the most recent stored
100
+ // messages, so it must run after the reply is persisted to reflect it. The
101
+ // service is itself fire-and-forget and re-checks replaceability, owning
102
+ // provider resolution, persistence, and the resulting broadcast.
103
+ setTimeout(() => {
104
+ queueRegenerateConversationTitle({ conversationId });
105
+ }, 0);
106
+ return;
107
+ }
108
+
109
+ if (retryFallbackTitle) {
110
+ const { conversationId } = ctx;
111
+ setTimeout(() => {
112
+ queueRegenerateConversationTitle({
113
+ conversationId,
114
+ onlyIfReplaceable: true,
115
+ });
116
+ }, 0);
117
+ return;
118
+ }
84
119
  };
85
120
 
86
121
  export default stop;
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { basename, dirname, join } from "node:path";
3
3
 
4
4
  import {
5
- findContactByChannelExternalId,
5
+ findContactByAddress,
6
6
  findGuardianForChannel,
7
7
  listGuardianChannels,
8
8
  } from "../contacts/contact-store.js";
@@ -88,7 +88,7 @@ function resolveUserFilename(
88
88
  }
89
89
  } else if (trustContext.requesterExternalUserId) {
90
90
  // Channel-routed request — look up contact by channel identity
91
- const contactWithChannels = findContactByChannelExternalId(
91
+ const contactWithChannels = findContactByAddress(
92
92
  trustContext.sourceChannel,
93
93
  trustContext.requesterExternalUserId,
94
94
  );
@@ -96,13 +96,23 @@ function resolveUserFilename(
96
96
  filename = contactWithChannels.userFile ?? null;
97
97
  } else if (trustContext.trustClass === "guardian") {
98
98
  // Managed desktop: the JWT principal ID used as requesterExternalUserId
99
- // may differ from the contact channel's external_user_id (they are
100
- // separate identity concepts). Fall back to the channel-type guardian.
99
+ // may differ from the contact channel's address (they are separate
100
+ // identity concepts). Fall back to the channel-type guardian.
101
101
  const guardian = findGuardianForChannel(trustContext.sourceChannel);
102
102
  if (guardian) {
103
103
  filename = guardian.contact.userFile ?? "guardian.md";
104
104
  }
105
105
  }
106
+ } else if (trustContext.trustClass === "guardian") {
107
+ // Guardian-trust turn carrying no requester identity — background and
108
+ // scheduled turns (heartbeat, scheduled pulses) run under the guardian
109
+ // trust class but have no per-actor address to look up. Resolve the
110
+ // channel's guardian user file so they load the same persona as a
111
+ // foreground guardian turn instead of falling back to users/default.md.
112
+ const guardian = findGuardianForChannel(trustContext.sourceChannel);
113
+ if (guardian) {
114
+ filename = guardian.contact.userFile ?? "guardian.md";
115
+ }
106
116
  }
107
117
  } catch (err) {
108
118
  // Contacts table may be absent — happens during early bootstrap
@@ -259,6 +259,13 @@ Meet your user where they are. If they are nontechnical, prefer "Gmail needs rec
259
259
  Err toward brevity; expand only when the user follows up or their style calls for more.
260
260
 
261
261
  These are default guidelines. Always prioritize communication preferences that you've established through your relationship with your human.
262
+ `,
263
+ },
264
+ {
265
+ id: "01-delegate-subagents",
266
+ body: `## Delegate independent work
267
+
268
+ When part of a task can run on its own — a research sweep, a multi-file investigation, a build-and-test loop — hand it off instead of grinding through it inline: load the \`subagent\` skill, then \`subagent_spawn\` early and in parallel. Make delegating that kind of work your default, not a last resort; an unnecessary subagent is cheaper than serialized work, and a long inline dig floods your own context.
262
269
  `,
263
270
  },
264
271
  {
@@ -267,8 +274,6 @@ These are default guidelines. Always prioritize communication preferences that y
267
274
  Batch independent tool calls into the same response. An extra LLM round trip costs orders of magnitude more than a few wasted tool calls — err on the side of parallelizing when calls are independent. Reading multiple files, \`glob\`/\`grep\`, \`ls\`, \`git status\`/\`diff\`/\`log\`, type-checks, and tests should be batched.
268
275
 
269
276
  Before emitting a single tool call, ask whether your next turn would be another tool call that doesn't consume this one's output — if so, they belong together. Serialized tool calls without a real data dependency are a bug.
270
-
271
- For non-trivial independent workstreams — research, coding, multi-step investigations — delegate to subagents (load the \`subagent\` skill) and spawn them early and in parallel; an unnecessary subagent is cheaper than serialized work.
272
277
  </use_parallel_tool_calls>
273
278
  `,
274
279
  },
@@ -35,6 +35,7 @@ describe("getLlmProviderEnvVar", () => {
35
35
  expect(getLlmProviderEnvVar("brave")).toBeUndefined();
36
36
  expect(getLlmProviderEnvVar("perplexity")).toBeUndefined();
37
37
  expect(getLlmProviderEnvVar("tavily")).toBeUndefined();
38
+ expect(getLlmProviderEnvVar("firecrawl")).toBeUndefined();
38
39
  });
39
40
 
40
41
  test("returns undefined for unknown provider", () => {
@@ -55,6 +56,10 @@ describe("getSearchProviderEnvVar", () => {
55
56
  expect(getSearchProviderEnvVar("tavily")).toBe("TAVILY_API_KEY");
56
57
  });
57
58
 
59
+ test("returns FIRECRAWL_API_KEY for firecrawl", () => {
60
+ expect(getSearchProviderEnvVar("firecrawl")).toBe("FIRECRAWL_API_KEY");
61
+ });
62
+
58
63
  test("returns undefined for LLM providers (out of scope)", () => {
59
64
  expect(getSearchProviderEnvVar("anthropic")).toBeUndefined();
60
65
  expect(getSearchProviderEnvVar("openai")).toBeUndefined();
@@ -75,6 +80,7 @@ describe("getAnyProviderEnvVar", () => {
75
80
  expect(getAnyProviderEnvVar("brave")).toBe("BRAVE_API_KEY");
76
81
  expect(getAnyProviderEnvVar("perplexity")).toBe("PERPLEXITY_API_KEY");
77
82
  expect(getAnyProviderEnvVar("tavily")).toBe("TAVILY_API_KEY");
83
+ expect(getAnyProviderEnvVar("firecrawl")).toBe("FIRECRAWL_API_KEY");
78
84
  });
79
85
 
80
86
  test("returns undefined for ollama (keyless LLM provider)", () => {
@@ -44,5 +44,6 @@ describe("API_KEY_PROVIDERS", () => {
44
44
  expect(API_KEY_PROVIDERS).toContain("brave");
45
45
  expect(API_KEY_PROVIDERS).toContain("perplexity");
46
46
  expect(API_KEY_PROVIDERS).toContain("tavily");
47
+ expect(API_KEY_PROVIDERS).toContain("firecrawl");
47
48
  });
48
49
  });