@vellumai/assistant 0.10.2 → 0.10.3-dev.202606252046.9075fd5

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 (546) hide show
  1. package/bun.lock +20 -0
  2. package/docs/workspace-tools.md +42 -33
  3. package/eslint-rules/cli-no-daemon-internals.js +6 -0
  4. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-delivery-contract.test.ts +91 -0
  5. package/node_modules/@vellumai/gateway-client/src/__tests__/trust-verdict-contract.test.ts +31 -0
  6. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +44 -0
  7. package/node_modules/@vellumai/gateway-client/src/guardian-delivery-contract.ts +48 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +14 -0
  9. package/node_modules/@vellumai/gateway-client/src/trust-verdict-contract.ts +17 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +28 -0
  12. package/node_modules/@vellumai/service-contracts/src/channels.ts +41 -0
  13. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  14. package/openapi.yaml +196 -56
  15. package/package.json +4 -1
  16. package/scripts/test.sh +36 -15
  17. package/src/__tests__/actor-token-service.test.ts +36 -14
  18. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
  19. package/src/__tests__/agent-loop-override-profile.test.ts +1 -0
  20. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  21. package/src/__tests__/agent-wake-override-profile.test.ts +2 -0
  22. package/src/__tests__/annotate-activity-metadata.test.ts +2 -0
  23. package/src/__tests__/annotate-risk-options.test.ts +2 -0
  24. package/src/__tests__/approval-cascade.test.ts +2 -0
  25. package/src/__tests__/assistant-attachments.test.ts +42 -0
  26. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -0
  27. package/src/__tests__/btw-routes.test.ts +2 -0
  28. package/src/__tests__/build-persisted-content.test.ts +2 -0
  29. package/src/__tests__/call-controller.test.ts +19 -0
  30. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  31. package/src/__tests__/channel-delivery-store.test.ts +28 -0
  32. package/src/__tests__/channel-guardian.test.ts +164 -78
  33. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  34. package/src/__tests__/channel-reply-delivery.test.ts +2 -0
  35. package/src/__tests__/code-search-tool.test.ts +585 -0
  36. package/src/__tests__/compaction-events.test.ts +2 -0
  37. package/src/__tests__/compaction-ledger-store.test.ts +128 -0
  38. package/src/__tests__/compaction.benchmark.test.ts +2 -0
  39. package/src/__tests__/compactor-call-site-logging.test.ts +2 -0
  40. package/src/__tests__/compactor-low-watermark-cut.test.ts +2 -0
  41. package/src/__tests__/compactor-preserved-tail-count.test.ts +2 -0
  42. package/src/__tests__/compactor-summary-call-truncation.test.ts +2 -0
  43. package/src/__tests__/compactor-web-search-strip.test.ts +2 -0
  44. package/src/__tests__/config-loader-backfill.test.ts +123 -10
  45. package/src/__tests__/config-schema.test.ts +1 -0
  46. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +31 -29
  47. package/src/__tests__/consult-deadline.test.ts +60 -0
  48. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  49. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  50. package/src/__tests__/contacts-relay-reads.test.ts +19 -24
  51. package/src/__tests__/contacts-write.test.ts +0 -2
  52. package/src/__tests__/conversation-abort-tool-results.test.ts +2 -0
  53. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +2 -0
  54. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  55. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  56. package/src/__tests__/conversation-agent-loop.test.ts +134 -0
  57. package/src/__tests__/conversation-analysis-routes.test.ts +2 -0
  58. package/src/__tests__/conversation-app-control-lifecycle.test.ts +2 -0
  59. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  60. package/src/__tests__/conversation-confirmation-signals.test.ts +2 -0
  61. package/src/__tests__/conversation-fork-crud.test.ts +354 -24
  62. package/src/__tests__/conversation-history-web-search.test.ts +2 -0
  63. package/src/__tests__/conversation-load-history-repair.test.ts +2 -0
  64. package/src/__tests__/conversation-load-history-stripped.test.ts +2 -0
  65. package/src/__tests__/conversation-pairing.test.ts +2 -0
  66. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +2 -0
  67. package/src/__tests__/conversation-process-callsite.test.ts +2 -0
  68. package/src/__tests__/conversation-provider-retry-repair.test.ts +2 -0
  69. package/src/__tests__/conversation-queue.test.ts +91 -0
  70. package/src/__tests__/conversation-routes-guardian-reply.test.ts +14 -0
  71. package/src/__tests__/conversation-routes-slash-commands.test.ts +14 -0
  72. package/src/__tests__/conversation-slash-queue.test.ts +2 -0
  73. package/src/__tests__/conversation-slash-unknown.test.ts +2 -0
  74. package/src/__tests__/conversation-speed-override.test.ts +2 -0
  75. package/src/__tests__/conversation-surfaces-task-progress.test.ts +29 -0
  76. package/src/__tests__/conversation-title-service.test.ts +2 -0
  77. package/src/__tests__/conversation-tool-setup-attribution.test.ts +47 -0
  78. package/src/__tests__/conversation-usage.test.ts +2 -0
  79. package/src/__tests__/conversation-workspace-cache-state.test.ts +2 -0
  80. package/src/__tests__/conversation-workspace-injection.test.ts +2 -0
  81. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +2 -0
  82. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  83. package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
  84. package/src/__tests__/db-migration-rollback.test.ts +205 -171
  85. package/src/__tests__/db-test-helpers.ts +5 -4
  86. package/src/__tests__/delete-propagation.test.ts +5 -3
  87. package/src/__tests__/deterministic-verification-control-plane.test.ts +4 -2
  88. package/src/__tests__/disk-pressure-guard.test.ts +41 -0
  89. package/src/__tests__/dm-backfill.test.ts +6 -4
  90. package/src/__tests__/dm-persistence.test.ts +2 -0
  91. package/src/__tests__/emit-signal-routing-intent.test.ts +7 -6
  92. package/src/__tests__/events-dev-bypass-actor.test.ts +7 -1
  93. package/src/__tests__/exploration-drift-hook.test.ts +3 -2
  94. package/src/__tests__/filing-service.test.ts +2 -0
  95. package/src/__tests__/guardian-binding-drift-heal.test.ts +104 -19
  96. package/src/__tests__/guardian-dispatch.test.ts +140 -1
  97. package/src/__tests__/guardian-expiry-notifier.test.ts +282 -0
  98. package/src/__tests__/guardian-outbound-http.test.ts +13 -0
  99. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  100. package/src/__tests__/heartbeat-disk-pressure.test.ts +2 -0
  101. package/src/__tests__/heartbeat-service.test.ts +2 -0
  102. package/src/__tests__/helpers/channel-test-adapter.ts +46 -19
  103. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  104. package/src/__tests__/helpers/mock-logger.ts +1 -0
  105. package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
  106. package/src/__tests__/host-app-control-routes.test.ts +24 -30
  107. package/src/__tests__/host-bash-routes.test.ts +31 -41
  108. package/src/__tests__/host-browser-routes.test.ts +26 -32
  109. package/src/__tests__/host-cu-routes-targeted.test.ts +25 -33
  110. package/src/__tests__/host-file-routes-targeted.test.ts +40 -52
  111. package/src/__tests__/host-transfer-routes-targeted.test.ts +31 -43
  112. package/src/__tests__/http-user-message-parity.test.ts +290 -8
  113. package/src/__tests__/inbound-invite-redemption.test.ts +115 -10
  114. package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
  115. package/src/__tests__/invite-redemption-service.test.ts +471 -53
  116. package/src/__tests__/invite-routes-http.test.ts +34 -0
  117. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  118. package/src/__tests__/llm-context-normalization.test.ts +105 -0
  119. package/src/__tests__/llm-request-log-error-payload.test.ts +71 -9
  120. package/src/__tests__/llm-usage-store.test.ts +25 -0
  121. package/src/__tests__/mcp-config-secret-boundary.test.ts +392 -0
  122. package/src/__tests__/mcp-health-check.test.ts +2 -1
  123. package/src/__tests__/media-stream-server-integration.test.ts +127 -0
  124. package/src/__tests__/memory-retrieval-hook.test.ts +2 -0
  125. package/src/__tests__/messaging-send-tool.test.ts +2 -0
  126. package/src/__tests__/migration-import-from-url.test.ts +2 -2
  127. package/src/__tests__/mtime-cache.test.ts +275 -5
  128. package/src/__tests__/native-web-search.test.ts +2 -0
  129. package/src/__tests__/non-member-access-request.test.ts +191 -17
  130. package/src/__tests__/notification-broadcaster.test.ts +4 -0
  131. package/src/__tests__/notification-decision-recipient-context.test.ts +33 -32
  132. package/src/__tests__/notification-deep-link.test.ts +6 -0
  133. package/src/__tests__/notification-guardian-path.test.ts +19 -0
  134. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  135. package/src/__tests__/openai-provider.test.ts +22 -12
  136. package/src/__tests__/openai-responses-provider.test.ts +12 -2
  137. package/src/__tests__/outbound-slack-persistence.test.ts +2 -0
  138. package/src/__tests__/pending-interactions-resolved-event.test.ts +7 -4
  139. package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
  140. package/src/__tests__/persistence-secret-redaction.test.ts +2 -0
  141. package/src/__tests__/persona-resolver.test.ts +75 -45
  142. package/src/__tests__/plugin-bootstrap.test.ts +21 -83
  143. package/src/__tests__/plugin-disabled-state.test.ts +190 -0
  144. package/src/__tests__/plugin-pipeline.test.ts +96 -0
  145. package/src/__tests__/plugin-route-contribution.test.ts +6 -19
  146. package/src/__tests__/plugin-tool-contribution.test.ts +5 -20
  147. package/src/__tests__/plugin-types.test.ts +2 -4
  148. package/src/__tests__/process-message-background-slack.test.ts +2 -0
  149. package/src/__tests__/process-message-display-content.test.ts +2 -0
  150. package/src/__tests__/provider-error-scenarios.test.ts +5 -4
  151. package/src/__tests__/provider-usage-tracking.test.ts +39 -0
  152. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  153. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
  154. package/src/__tests__/reaction-persistence.test.ts +51 -4
  155. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +2 -0
  156. package/src/__tests__/registry.test.ts +4 -1
  157. package/src/__tests__/relay-server.test.ts +758 -32
  158. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -12
  159. package/src/__tests__/secret-ingress-http.test.ts +14 -0
  160. package/src/__tests__/send-endpoint-busy.test.ts +30 -8
  161. package/src/__tests__/settings-routes.test.ts +32 -0
  162. package/src/__tests__/skills.test.ts +44 -0
  163. package/src/__tests__/slack-inbound-verification.test.ts +47 -2
  164. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +79 -0
  165. package/src/__tests__/steer-on-enqueue-question.test.ts +181 -0
  166. package/src/__tests__/stt-hints.test.ts +49 -15
  167. package/src/__tests__/subagent-detail.test.ts +27 -0
  168. package/src/__tests__/subagent-disposal.test.ts +65 -0
  169. package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
  170. package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
  171. package/src/__tests__/subagent-notify-parent.test.ts +2 -0
  172. package/src/__tests__/subagent-role-registry.test.ts +24 -6
  173. package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
  174. package/src/__tests__/subagent-spawn-tool-fork.test.ts +2 -0
  175. package/src/__tests__/subagent-tools.test.ts +398 -1
  176. package/src/__tests__/suggestion-routes.test.ts +2 -0
  177. package/src/__tests__/thread-backfill.test.ts +3 -3
  178. package/src/__tests__/title-generate-hook.test.ts +2 -0
  179. package/src/__tests__/tool-executor-lifecycle-events.test.ts +2 -0
  180. package/src/__tests__/tool-executor.test.ts +16 -11
  181. package/src/__tests__/tool-preview-lifecycle.test.ts +2 -0
  182. package/src/__tests__/tool-result-metadata-plumbing.test.ts +2 -0
  183. package/src/__tests__/tool-start-timestamp.test.ts +2 -0
  184. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  185. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +12 -12
  186. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  187. package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
  188. package/src/__tests__/trusted-contact-verification.test.ts +79 -54
  189. package/src/__tests__/twilio-routes.test.ts +96 -0
  190. package/src/__tests__/ui-file-upload-surface.test.ts +86 -0
  191. package/src/__tests__/verification-control-plane-policy.test.ts +2 -0
  192. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
  193. package/src/__tests__/voice-invite-redemption.test.ts +216 -20
  194. package/src/__tests__/web-search-backend-failure.test.ts +2 -0
  195. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
  196. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +208 -0
  197. package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
  198. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  199. package/src/__tests__/workspace-migration-remove-hooks.test.ts +14 -35
  200. package/src/__tests__/workspace-tool-loader.test.ts +195 -2
  201. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
  202. package/src/agent/loop-exclusive-tool.test.ts +154 -0
  203. package/src/agent/loop-native-web-search.test.ts +200 -0
  204. package/src/agent/loop.ts +164 -1
  205. package/src/api/constants/sse-replay.ts +41 -0
  206. package/src/api/events/conversation-notice.ts +26 -0
  207. package/src/api/index.ts +19 -1
  208. package/src/api/responses/llm-request-log-entry.ts +29 -0
  209. package/src/api/responses/subagent-detail.ts +17 -0
  210. package/src/api/surfaces.ts +39 -3
  211. package/src/approvals/guardian-channel-delivery.ts +30 -0
  212. package/src/approvals/guardian-expiry-notifier.ts +148 -0
  213. package/src/approvals/guardian-request-resolvers.ts +17 -15
  214. package/src/calls/__tests__/relay-setup-router.test.ts +267 -17
  215. package/src/calls/call-domain.ts +3 -3
  216. package/src/calls/guardian-dispatch.ts +14 -9
  217. package/src/calls/inbound-trust-reader.ts +24 -2
  218. package/src/calls/media-stream-server.ts +21 -0
  219. package/src/calls/relay-access-wait.ts +6 -6
  220. package/src/calls/relay-server.ts +188 -51
  221. package/src/calls/relay-setup-router.ts +47 -17
  222. package/src/calls/relay-verification.ts +4 -4
  223. package/src/calls/stt-hints.ts +9 -12
  224. package/src/calls/twilio-routes.ts +14 -4
  225. package/src/channels/types.ts +10 -20
  226. package/src/cli/commands/__tests__/cache.test.ts +8 -1
  227. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  228. package/src/cli/commands/cache.ts +194 -181
  229. package/src/cli/commands/contacts.ts +10 -7
  230. package/src/cli/commands/db/__tests__/repair.test.ts +6 -5
  231. package/src/cli/commands/db/status.ts +37 -1
  232. package/src/cli/commands/mcp.ts +252 -218
  233. package/src/cli/commands/memory/__tests__/worker.test.ts +432 -0
  234. package/src/cli/commands/memory/index.ts +2 -0
  235. package/src/cli/commands/memory/worker.ts +242 -0
  236. package/src/cli/commands/plugins.ts +125 -13
  237. package/src/cli/lib/__tests__/install-from-github.test.ts +102 -0
  238. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +160 -1
  239. package/src/cli/lib/__tests__/publish-plugin.test.ts +404 -0
  240. package/src/cli/lib/list-installed-plugins.ts +179 -1
  241. package/src/cli/lib/publish-plugin.ts +633 -0
  242. package/src/config/__tests__/loader-callsite-strip-fallback.test.ts +143 -0
  243. package/src/config/__tests__/sync-gated-profiles.test.ts +16 -10
  244. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +27 -17
  245. package/src/config/bundled-skills/contacts/tools/contact-search.ts +13 -3
  246. package/src/config/bundled-skills/subagent/SKILL.md +17 -2
  247. package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
  248. package/src/config/call-site-defaults.ts +0 -6
  249. package/src/config/feature-flag-registry.json +5 -13
  250. package/src/config/llm-resolver.ts +0 -3
  251. package/src/config/loader.ts +38 -5
  252. package/src/config/prune-seeded-callsite-defaults.ts +110 -0
  253. package/src/config/schemas/__tests__/memory-v3.test.ts +1 -0
  254. package/src/config/schemas/call-site-catalog.ts +0 -7
  255. package/src/config/schemas/heartbeat.ts +2 -5
  256. package/src/config/schemas/llm.ts +3 -12
  257. package/src/config/schemas/memory-lifecycle.ts +12 -0
  258. package/src/config/schemas/memory-v3.ts +7 -0
  259. package/src/config/schemas/memory.ts +4 -0
  260. package/src/config/schemas/timeouts.ts +8 -0
  261. package/src/config/seed-inference-profiles.ts +30 -34
  262. package/src/config/skills.ts +27 -5
  263. package/src/config/sync-gated-profiles.ts +12 -16
  264. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +128 -0
  265. package/src/contacts/__tests__/guardian-delivery-reader.test.ts +312 -0
  266. package/src/contacts/__tests__/member-write-relay.test.ts +226 -0
  267. package/src/contacts/contact-store.ts +27 -237
  268. package/src/contacts/contacts-write.ts +18 -55
  269. package/src/contacts/gateway-channel-read.ts +51 -0
  270. package/src/contacts/guardian-delivery-reader.ts +223 -0
  271. package/src/contacts/member-write-relay.ts +183 -0
  272. package/src/contacts/types.ts +3 -15
  273. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +35 -0
  274. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
  275. package/src/daemon/assistant-attachments.ts +27 -4
  276. package/src/daemon/conversation-agent-loop.ts +28 -0
  277. package/src/daemon/conversation-notices.ts +60 -0
  278. package/src/daemon/conversation-process.ts +35 -16
  279. package/src/daemon/conversation-surfaces.ts +111 -38
  280. package/src/daemon/conversation-tool-setup.ts +71 -30
  281. package/src/daemon/conversation.ts +23 -1
  282. package/src/daemon/disk-pressure-guard.ts +12 -2
  283. package/src/daemon/event-loop-watchdog.ts +28 -1
  284. package/src/daemon/external-plugins-bootstrap.ts +17 -41
  285. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  286. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  287. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +25 -2
  288. package/src/daemon/handlers/__tests__/config-channels.test.ts +220 -0
  289. package/src/daemon/handlers/config-a2a.ts +6 -14
  290. package/src/daemon/handlers/config-channels.ts +67 -26
  291. package/src/daemon/handlers/conversations.ts +77 -0
  292. package/src/daemon/lifecycle.ts +16 -0
  293. package/src/daemon/mcp-reload-service.ts +10 -0
  294. package/src/daemon/memory-v2-startup.test.ts +72 -0
  295. package/src/daemon/memory-v2-startup.ts +87 -19
  296. package/src/daemon/message-types/conversations.ts +2 -0
  297. package/src/daemon/message-types/surfaces.ts +12 -12
  298. package/src/daemon/server.ts +0 -4
  299. package/src/daemon/shutdown-handlers.ts +20 -0
  300. package/src/daemon/tool-setup-types.ts +9 -0
  301. package/src/heartbeat/heartbeat-service.ts +5 -0
  302. package/src/home/relationship-state-writer.ts +5 -0
  303. package/src/hooks/hook-loader.ts +341 -0
  304. package/src/ipc/__tests__/clients-list-ipc.test.ts +1 -1
  305. package/src/ipc/assistant-server.ts +2 -2
  306. package/src/mcp/__tests__/mcp-auth-orchestrator.test.ts +1 -0
  307. package/src/mcp/client.ts +15 -1
  308. package/src/mcp/mcp-auth-orchestrator.ts +6 -1
  309. package/src/mcp/mcp-header-store.ts +134 -0
  310. package/src/mcp/mcp-oauth-provider.ts +19 -8
  311. package/src/memory/__tests__/301-create-watchdog-events.test.ts +110 -0
  312. package/src/memory/__tests__/memory-retrospective-job.test.ts +8 -0
  313. package/src/memory/__tests__/prompt-override.test.ts +192 -0
  314. package/src/memory/__tests__/watchdog-events-store.test.ts +161 -0
  315. package/src/memory/compaction-ledger-store.ts +107 -0
  316. package/src/memory/conversation-crud.ts +129 -61
  317. package/src/memory/db-connection.ts +22 -3
  318. package/src/memory/db-init.ts +37 -503
  319. package/src/memory/db-singleton.ts +6 -4
  320. package/src/memory/jobs-worker.ts +104 -0
  321. package/src/memory/llm-request-log-store.ts +26 -1
  322. package/src/memory/llm-usage-store.ts +48 -20
  323. package/src/memory/memory-retrospective-job.ts +14 -8
  324. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +152 -56
  325. package/src/memory/migrations/300-add-processing-started-at.ts +30 -0
  326. package/src/memory/migrations/301-create-watchdog-events.ts +45 -0
  327. package/src/memory/migrations/302-create-compaction-events.ts +107 -0
  328. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +297 -0
  329. package/src/memory/migrations/__tests__/run-migrations.test.ts +2 -2
  330. package/src/memory/migrations/run-migrations.ts +106 -10
  331. package/src/memory/migrations/validate-migration-state.ts +105 -67
  332. package/src/memory/prompt-override.ts +129 -0
  333. package/src/memory/schema/contacts.ts +6 -2
  334. package/src/memory/schema/conversations.ts +37 -0
  335. package/src/memory/schema/infrastructure.ts +20 -0
  336. package/src/memory/steps.ts +1294 -0
  337. package/src/memory/v2/__tests__/cli-command-store.test.ts +25 -0
  338. package/src/memory/v2/__tests__/skill-store.test.ts +80 -0
  339. package/src/memory/v2/cli-command-store.ts +75 -38
  340. package/src/memory/v2/prompts/consolidation.ts +13 -82
  341. package/src/memory/v2/prompts/router.ts +21 -93
  342. package/src/memory/v2/skill-store.ts +68 -31
  343. package/src/memory/watchdog-events-store.ts +87 -0
  344. package/src/memory/worker-control.ts +204 -0
  345. package/src/memory/worker-process.ts +89 -0
  346. package/src/messaging/channel-binding-metadata.ts +31 -0
  347. package/src/messaging/provider-types.ts +8 -0
  348. package/src/messaging/providers/slack/binding-metadata.ts +60 -0
  349. package/src/notifications/__tests__/broadcaster.test.ts +8 -8
  350. package/src/notifications/__tests__/connected-channels.test.ts +86 -0
  351. package/src/notifications/__tests__/decision-engine.test.ts +78 -9
  352. package/src/notifications/__tests__/destination-resolver.test.ts +151 -0
  353. package/src/notifications/broadcaster.ts +8 -1
  354. package/src/notifications/decision-engine.ts +15 -7
  355. package/src/notifications/destination-resolver.ts +53 -25
  356. package/src/notifications/emit-signal.ts +34 -15
  357. package/src/onboarding/checkin-event.test.ts +222 -0
  358. package/src/onboarding/checkin-event.ts +321 -0
  359. package/src/onboarding/schedule-checkin.ts +190 -0
  360. package/src/permissions/question-prompter.test.ts +1 -1
  361. package/src/permissions/question-prompter.ts +7 -4
  362. package/src/plugin-api/index.ts +12 -12
  363. package/src/plugin-api/types.ts +12 -14
  364. package/src/plugin-api/vision-support.test.ts +28 -4
  365. package/src/plugin-api/vision-support.ts +66 -31
  366. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +2 -2
  367. package/src/plugins/defaults/empty-response/hooks/stop.ts +2 -2
  368. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +4 -3
  369. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +2 -2
  370. package/src/plugins/defaults/history-repair/hooks/stop.ts +2 -2
  371. package/src/plugins/defaults/history-repair/hooks/user-prompt-submit.ts +2 -2
  372. package/src/plugins/defaults/image-fallback/__tests__/image-fallback.test.ts +47 -7
  373. package/src/plugins/defaults/image-fallback/hooks/post-tool-use.ts +12 -13
  374. package/src/plugins/defaults/image-fallback/hooks/user-prompt-submit.ts +14 -22
  375. package/src/plugins/defaults/image-fallback/src/caption-blocks.ts +42 -11
  376. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +2 -2
  377. package/src/plugins/defaults/image-recovery/hooks/stop.ts +2 -2
  378. package/src/plugins/defaults/index.ts +0 -35
  379. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +2 -2
  380. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +2 -2
  381. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +2 -2
  382. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +2 -2
  383. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +33 -3
  384. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +48 -4
  385. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +4 -8
  386. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +2 -2
  387. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +2 -2
  388. package/src/plugins/defaults/memory-v3-shadow/injector.ts +43 -15
  389. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +11 -2
  390. package/src/plugins/defaults/memory-v3-shadow/pool-select.test.ts +146 -0
  391. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +77 -13
  392. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +12 -11
  393. package/src/plugins/defaults/surface-completion-nudge/hooks/post-model-call.ts +2 -2
  394. package/src/plugins/defaults/surface-completion-nudge/hooks/stop.ts +2 -2
  395. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +2 -2
  396. package/src/plugins/defaults/title-generate/hooks/stop.ts +2 -2
  397. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +2 -2
  398. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +2 -2
  399. package/src/plugins/defaults/tool-result-truncate/hooks/post-tool-use.ts +2 -2
  400. package/src/plugins/disabled-state.ts +31 -0
  401. package/src/plugins/external-plugin-loader.ts +2 -2
  402. package/src/plugins/mtime-cache.ts +186 -330
  403. package/src/plugins/pipeline.ts +111 -13
  404. package/src/plugins/registry.ts +59 -16
  405. package/src/plugins/surface-import.ts +121 -0
  406. package/src/plugins/types.ts +7 -9
  407. package/src/prompts/persona-resolver.ts +43 -11
  408. package/src/providers/anthropic/client.ts +5 -0
  409. package/src/providers/call-site-routing.ts +45 -0
  410. package/src/providers/model-catalog.ts +16 -0
  411. package/src/providers/openai/__tests__/api-error-normalization.test.ts +321 -0
  412. package/src/providers/openai/api-error-normalization.ts +270 -0
  413. package/src/providers/openai/chat-completions-provider.ts +37 -83
  414. package/src/providers/openai/responses-provider.ts +50 -46
  415. package/src/providers/openrouter/client.ts +5 -0
  416. package/src/providers/provider-send-message.ts +10 -0
  417. package/src/providers/ratelimit.ts +10 -0
  418. package/src/providers/registry.ts +1 -1
  419. package/src/providers/retry.ts +10 -0
  420. package/src/providers/types.ts +22 -0
  421. package/src/providers/usage-tracking.ts +10 -0
  422. package/src/runtime/__tests__/channel-verification-service.test.ts +133 -0
  423. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +184 -0
  424. package/src/runtime/__tests__/is-guardian-bound-for-channel.test.ts +66 -0
  425. package/src/runtime/__tests__/local-principal-trust.test.ts +162 -0
  426. package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
  427. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +402 -123
  428. package/src/runtime/access-request-helper.ts +18 -39
  429. package/src/runtime/actor-trust-resolver.ts +46 -19
  430. package/src/runtime/anchored-guardian.test.ts +109 -0
  431. package/src/runtime/anchored-guardian.ts +86 -0
  432. package/src/runtime/assistant-event-hub.ts +1 -1
  433. package/src/runtime/assistant-stream-state.ts +9 -2
  434. package/src/runtime/auth/__tests__/require-bound-guardian.test.ts +99 -0
  435. package/src/runtime/auth/require-bound-guardian.ts +21 -11
  436. package/src/runtime/channel-verification-service.ts +56 -31
  437. package/src/runtime/confirmation-request-guardian-bridge.ts +3 -3
  438. package/src/runtime/guardian-vellum-migration.ts +69 -8
  439. package/src/runtime/invite-redemption-service.ts +213 -187
  440. package/src/runtime/local-actor-identity.test.ts +108 -0
  441. package/src/runtime/local-actor-identity.ts +85 -13
  442. package/src/runtime/local-principal-trust.ts +52 -0
  443. package/src/runtime/member-verdict-cache.ts +0 -0
  444. package/src/runtime/pending-interactions.ts +11 -1
  445. package/src/runtime/routes/__tests__/channel-verification-revoke.test.ts +56 -5
  446. package/src/runtime/routes/__tests__/channel-verification-routes.test.ts +1 -1
  447. package/src/runtime/routes/__tests__/contact-routes.test.ts +305 -0
  448. package/src/runtime/routes/__tests__/global-search-routes.test.ts +92 -0
  449. package/src/runtime/routes/__tests__/http-adapter-actor-header.test.ts +129 -0
  450. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +188 -0
  451. package/src/runtime/routes/browser-routes.ts +1 -1
  452. package/src/runtime/routes/canonical-guardian-expiry-sweep.ts +13 -5
  453. package/src/runtime/routes/channel-verification-routes.ts +3 -3
  454. package/src/runtime/routes/contact-routes.ts +48 -57
  455. package/src/runtime/routes/conversation-cli-routes.ts +4 -5
  456. package/src/runtime/routes/conversation-list-routes.ts +4 -7
  457. package/src/runtime/routes/conversation-query-routes.ts +72 -0
  458. package/src/runtime/routes/conversation-routes.ts +84 -85
  459. package/src/runtime/routes/events-routes.ts +2 -2
  460. package/src/runtime/routes/global-search-routes.ts +3 -1
  461. package/src/runtime/routes/guardian-action-routes.ts +4 -5
  462. package/src/runtime/routes/host-app-control-routes.ts +5 -4
  463. package/src/runtime/routes/host-bash-routes.ts +5 -4
  464. package/src/runtime/routes/host-browser-routes.ts +9 -11
  465. package/src/runtime/routes/host-cu-routes.ts +5 -4
  466. package/src/runtime/routes/host-file-routes.ts +5 -4
  467. package/src/runtime/routes/host-transfer-routes.ts +6 -6
  468. package/src/runtime/routes/http-adapter.ts +16 -1
  469. package/src/runtime/routes/identity-routes.ts +3 -2
  470. package/src/runtime/routes/inbound-message-handler.ts +5 -5
  471. package/src/runtime/routes/inbound-stages/acl-enforcement.test.ts +97 -5
  472. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +61 -59
  473. package/src/runtime/routes/inbound-stages/background-dispatch.ts +13 -5
  474. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +7 -7
  475. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.test.ts +21 -8
  476. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +14 -3
  477. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
  478. package/src/runtime/routes/index.ts +2 -0
  479. package/src/runtime/routes/llm-context-normalization.ts +83 -0
  480. package/src/runtime/routes/mcp-auth-routes.ts +171 -19
  481. package/src/runtime/routes/migration-rollback-routes.ts +4 -3
  482. package/src/runtime/routes/migration-routes.ts +4 -1
  483. package/src/runtime/routes/onboarding-checkin-routes.ts +86 -0
  484. package/src/runtime/routes/settings-routes.ts +8 -3
  485. package/src/runtime/routes/subagents-routes.ts +5 -0
  486. package/src/runtime/routes/surface-action-routes.ts +42 -56
  487. package/src/runtime/services/__tests__/conversation-serializer.test.ts +1 -0
  488. package/src/runtime/services/conversation-serializer.ts +13 -58
  489. package/src/runtime/tool-grant-request-helper.ts +3 -3
  490. package/src/runtime/trust-verdict-consumer.ts +111 -40
  491. package/src/runtime/verification-outbound-actions.ts +18 -18
  492. package/src/signals/user-message.ts +16 -0
  493. package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
  494. package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
  495. package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +22 -32
  496. package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
  497. package/src/subagent/index.ts +1 -1
  498. package/src/subagent/manager.ts +254 -33
  499. package/src/subagent/types.ts +11 -4
  500. package/src/telemetry/types.ts +34 -1
  501. package/src/telemetry/usage-telemetry-reporter.test.ts +3 -2
  502. package/src/telemetry/usage-telemetry-reporter.ts +87 -3
  503. package/src/tools/ask-question/ask-question-tool.test.ts +29 -0
  504. package/src/tools/ask-question/ask-question-tool.ts +13 -0
  505. package/src/tools/executor.ts +4 -4
  506. package/src/tools/filesystem/search.ts +543 -0
  507. package/src/tools/registry.ts +38 -0
  508. package/src/tools/shared/filesystem/path-policy.ts +12 -5
  509. package/src/tools/subagent/consult-deadline.ts +49 -0
  510. package/src/tools/subagent/spawn.ts +234 -5
  511. package/src/tools/tool-approval-handler.ts +1 -1
  512. package/src/tools/tool-defaults.ts +9 -2
  513. package/src/tools/tool-manifest.ts +3 -0
  514. package/src/tools/types.ts +131 -25
  515. package/src/tools/workspace-tools/loader.ts +348 -244
  516. package/src/util/errors.ts +26 -1
  517. package/src/util/logger.ts +9 -0
  518. package/src/util/platform.ts +19 -0
  519. package/src/util/telemetry-db-path.ts +24 -0
  520. package/src/workflows/library.test.ts +140 -0
  521. package/src/workflows/library.ts +82 -28
  522. package/src/workspace/migrations/017-seed-persona-dirs.ts +3 -34
  523. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +3 -24
  524. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
  525. package/src/workspace/migrations/048-remove-workspace-hooks.ts +14 -66
  526. package/src/workspace/migrations/111-prune-seeded-callsite-defaults.ts +134 -0
  527. package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
  528. package/src/workspace/migrations/registry.ts +4 -0
  529. package/src/__tests__/workspace-tools-watcher-flag.test.ts +0 -70
  530. package/src/daemon/workspace-tools-watcher.ts +0 -328
  531. package/src/memory/migrations/registry.ts +0 -573
  532. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
  533. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
  534. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
  535. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -153
  536. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
  537. package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
  538. package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
  539. package/src/plugins/defaults/advisor/config.ts +0 -21
  540. package/src/plugins/defaults/advisor/consult.ts +0 -93
  541. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
  542. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
  543. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
  544. package/src/plugins/defaults/advisor/package.json +0 -14
  545. package/src/plugins/defaults/advisor/tools/advisor.ts +0 -65
  546. package/src/providers/openai/__tests__/api-error-detail.test.ts +0 -120
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Verifies the agent loop's provider-native web-search gate (used by the
3
+ * tool-less advisor consult): when `enableNativeWebSearch` is set, the loop
4
+ * appends a `web_search`-named SERVER tool to the outbound request and forces
5
+ * `tool_choice: auto` — but ONLY when the provider/model the call ACTUALLY
6
+ * routes to reports native-search support. A non-native target gets nothing,
7
+ * and the consult stays tool-less (no client `web_search` tool surfaced).
8
+ *
9
+ * The gate prefers the routing-aware `supportsNativeWebSearchFor(options)` (the
10
+ * routed (provider, model)'s capability) over the construction-time
11
+ * `supportsNativeWebSearch` snapshot. The advisor's `advisorProfile` can route
12
+ * `subagentSpawn` to a provider/model whose native-search support DIFFERS from
13
+ * the default, so the routed capability — not the default's — must drive the
14
+ * decision in both directions. Drives the REAL loop, mocking only the provider
15
+ * boundary.
16
+ */
17
+ import { describe, expect, test } from "bun:test";
18
+
19
+ import { createMockProvider } from "../__tests__/helpers/mock-provider.js";
20
+ import type {
21
+ Provider,
22
+ ProviderResponse,
23
+ SendMessageOptions,
24
+ } from "../providers/types.js";
25
+ import { AgentLoop } from "./loop.js";
26
+
27
+ const endTurn = (text: string): ProviderResponse => ({
28
+ content: [{ type: "text", text }],
29
+ model: "mock-model",
30
+ usage: { inputTokens: 1, outputTokens: 1 },
31
+ stopReason: "end_turn",
32
+ });
33
+
34
+ const baseRun = {
35
+ requestId: "req-web",
36
+ onEvent: () => {},
37
+ callSite: "subagentSpawn" as const,
38
+ trust: { sourceChannel: "vellum" as const, trustClass: "unknown" as const },
39
+ };
40
+
41
+ const userMessages = [
42
+ {
43
+ role: "user" as const,
44
+ content: [{ type: "text" as const, text: "advise" }],
45
+ },
46
+ ];
47
+
48
+ /** Build a tool-less loop (mirrors the advisor consult) with the given flag. */
49
+ function buildAdvisorLoop(
50
+ provider: Provider,
51
+ enableNativeWebSearch: boolean,
52
+ ): AgentLoop {
53
+ return new AgentLoop({
54
+ provider,
55
+ systemPrompt: "advisor system",
56
+ conversationId: "advisor-1",
57
+ // Tool-less for client tools — exactly the advisor role's empty allowlist.
58
+ config: { enableNativeWebSearch },
59
+ });
60
+ }
61
+
62
+ describe("AgentLoop — provider-native web search gate", () => {
63
+ test("attaches the native web_search server tool when the provider supports it", async () => {
64
+ const { provider, calls } = createMockProvider([endTurn("guidance")]);
65
+ (
66
+ provider as { supportsNativeWebSearch?: boolean }
67
+ ).supportsNativeWebSearch = true;
68
+
69
+ const loop = buildAdvisorLoop(provider, true);
70
+ await loop.run({ ...baseRun, messages: userMessages });
71
+
72
+ expect(calls).toHaveLength(1);
73
+ const sent = calls[0];
74
+ // The native web_search SERVER tool is the only tool surfaced.
75
+ expect(sent.tools?.map((t) => t.name)).toEqual(["web_search"]);
76
+ // tool_choice is forced to auto so the model may invoke the search.
77
+ expect(sent.options?.config?.tool_choice).toEqual({ type: "auto" });
78
+ });
79
+
80
+ test("attaches nothing on a non-native provider (consult stays tool-less)", async () => {
81
+ const { provider, calls } = createMockProvider([endTurn("guidance")]);
82
+ // supportsNativeWebSearch is absent (falsy) — a non-native provider.
83
+
84
+ const loop = buildAdvisorLoop(provider, true);
85
+ await loop.run({ ...baseRun, messages: userMessages });
86
+
87
+ expect(calls).toHaveLength(1);
88
+ const sent = calls[0];
89
+ // No web_search tool surfaced — no client tool the one-shot consult can't run.
90
+ expect(sent.tools).toBeUndefined();
91
+ // No tool_choice forced when nothing is attached.
92
+ expect(sent.options?.config?.tool_choice).toBeUndefined();
93
+ });
94
+
95
+ test("attaches nothing when the flag is off, even on a native provider", async () => {
96
+ const { provider, calls } = createMockProvider([endTurn("guidance")]);
97
+ (
98
+ provider as { supportsNativeWebSearch?: boolean }
99
+ ).supportsNativeWebSearch = true;
100
+
101
+ const loop = buildAdvisorLoop(provider, false);
102
+ await loop.run({ ...baseRun, messages: userMessages });
103
+
104
+ expect(calls).toHaveLength(1);
105
+ const sent = calls[0];
106
+ expect(sent.tools).toBeUndefined();
107
+ expect(sent.options?.config?.tool_choice).toBeUndefined();
108
+ });
109
+
110
+ test("does not duplicate web_search when a tool of that name is already present", async () => {
111
+ const { provider, calls } = createMockProvider([endTurn("guidance")]);
112
+ (
113
+ provider as { supportsNativeWebSearch?: boolean }
114
+ ).supportsNativeWebSearch = true;
115
+
116
+ // A loop that already exposes a `web_search` client tool (e.g. researcher
117
+ // role). The gate must not append a second `web_search` entry.
118
+ const loop = new AgentLoop({
119
+ provider,
120
+ systemPrompt: "sys",
121
+ conversationId: "advisor-dup",
122
+ config: { enableNativeWebSearch: true },
123
+ tools: [
124
+ {
125
+ name: "web_search",
126
+ description: "",
127
+ input_schema: { type: "object" },
128
+ },
129
+ ],
130
+ });
131
+ await loop.run({ ...baseRun, messages: userMessages });
132
+
133
+ const sent = calls[0];
134
+ expect(sent.tools?.filter((t) => t.name === "web_search")).toHaveLength(1);
135
+ });
136
+
137
+ // ── Routed capability drives the decision (not the static default) ────────
138
+
139
+ test("false positive: static flag is native but the ROUTED target is not — attaches nothing", async () => {
140
+ const { provider, calls } = createMockProvider([endTurn("guidance")]);
141
+ // The construction-time default supports native search…
142
+ (
143
+ provider as { supportsNativeWebSearch?: boolean }
144
+ ).supportsNativeWebSearch = true;
145
+ // …but the advisorProfile routes `subagentSpawn` to a provider/model that
146
+ // does NOT. The routing-aware probe wins, so no unexecutable client tool is
147
+ // surfaced to the otherwise tool-less advisor.
148
+ (
149
+ provider as {
150
+ supportsNativeWebSearchFor?: (o?: SendMessageOptions) => boolean;
151
+ }
152
+ ).supportsNativeWebSearchFor = () => false;
153
+
154
+ const loop = buildAdvisorLoop(provider, true);
155
+ await loop.run({ ...baseRun, messages: userMessages });
156
+
157
+ const sent = calls[0];
158
+ expect(sent.tools).toBeUndefined();
159
+ expect(sent.options?.config?.tool_choice).toBeUndefined();
160
+ });
161
+
162
+ test("false negative: static flag is non-native but the ROUTED target is — attaches the tool", async () => {
163
+ const { provider, calls } = createMockProvider([endTurn("guidance")]);
164
+ // The construction-time default lacks native search (flag absent/falsy)…
165
+ // …but the advisorProfile routes to a provider/model that has it.
166
+ (
167
+ provider as {
168
+ supportsNativeWebSearchFor?: (o?: SendMessageOptions) => boolean;
169
+ }
170
+ ).supportsNativeWebSearchFor = () => true;
171
+
172
+ const loop = buildAdvisorLoop(provider, true);
173
+ await loop.run({ ...baseRun, messages: userMessages });
174
+
175
+ const sent = calls[0];
176
+ expect(sent.tools?.map((t) => t.name)).toEqual(["web_search"]);
177
+ expect(sent.options?.config?.tool_choice).toEqual({ type: "auto" });
178
+ });
179
+
180
+ test("the routing probe receives the loop's callSite", async () => {
181
+ const { provider, calls } = createMockProvider([endTurn("guidance")]);
182
+ const probeOptions: (SendMessageOptions | undefined)[] = [];
183
+ (
184
+ provider as {
185
+ supportsNativeWebSearchFor?: (o?: SendMessageOptions) => boolean;
186
+ }
187
+ ).supportsNativeWebSearchFor = (o) => {
188
+ probeOptions.push(o);
189
+ return true;
190
+ };
191
+
192
+ const loop = buildAdvisorLoop(provider, true);
193
+ await loop.run({ ...baseRun, messages: userMessages });
194
+
195
+ expect(calls).toHaveLength(1);
196
+ // The probe is resolved against the same callSite the dispatch uses
197
+ // (`subagentSpawn` per `baseRun`), so the routed (provider, model) matches.
198
+ expect(probeOptions[0]?.config?.callSite).toBe("subagentSpawn");
199
+ });
200
+ });
package/src/agent/loop.ts CHANGED
@@ -95,6 +95,74 @@ export interface AgentLoopConfig {
95
95
  minTurnIntervalMs?: number;
96
96
  /** Override the default prompt cache TTL sent to the provider (e.g. "5m" for short-lived subagents). */
97
97
  cacheTtl?: "5m" | "1h";
98
+ /**
99
+ * Give every LLM call provider-native (server-side) web search, gated on the
100
+ * native-search capability of the (provider, model) the call routes to —
101
+ * {@link Provider.supportsNativeWebSearchFor} when the provider exposes it,
102
+ * else the static {@link Provider.supportsNativeWebSearch} flag.
103
+ * When both are true, the loop appends a `web_search`-named tool to the
104
+ * outbound request — which Anthropic/OpenAI substitute for their server-side
105
+ * search tool, running the search inline and returning results without a
106
+ * client tool round-trip — and forces `tool_choice: auto` so the model may
107
+ * call it. Non-native providers get nothing.
108
+ *
109
+ * This is a SERVER tool the provider runs itself, distinct from the client
110
+ * tool list (`tools` / `resolveTools`): it is never executed by
111
+ * {@link AgentLoopConstructorOptions.toolExecutor} and does not require any
112
+ * client-tool allowlist entry. Used by the tool-less advisor consult to
113
+ * ground its guidance with live web access while staying one-shot for client
114
+ * tools. Defaults to false — existing behavior.
115
+ */
116
+ enableNativeWebSearch?: boolean;
117
+ }
118
+
119
+ /**
120
+ * The `web_search`-named tool the loop appends when
121
+ * {@link AgentLoopConfig.enableNativeWebSearch} is set on a native provider.
122
+ * Anthropic/OpenAI intercept a tool with this name and substitute their own
123
+ * server-side web search (run inline, no client execution), so the exact
124
+ * `input_schema` is informational — the provider supplies the real schema.
125
+ */
126
+ const NATIVE_WEB_SEARCH_TOOL: ToolDefinition = {
127
+ name: "web_search",
128
+ description:
129
+ "Search the web for current information to ground your response.",
130
+ input_schema: {
131
+ type: "object",
132
+ properties: {
133
+ query: { type: "string", description: "The search query." },
134
+ },
135
+ required: ["query"],
136
+ },
137
+ };
138
+
139
+ /**
140
+ * Build the minimal `SendMessageOptions` a routing-aware provider needs to
141
+ * report the native web-search capability of the (provider, model) THIS turn
142
+ * routes to. Mirrors the call-site fields the loop plumbs onto the actual send
143
+ * (`callSite` + `overrideProfile`/`forceOverrideProfile` + per-conversation
144
+ * `selectionSeed`) so the capability probe and the dispatch resolve the same
145
+ * arm. Returns `undefined` when there is no `callSite` (the legacy
146
+ * default-provider path); `selectionSeed` is omitted for standalone loops with
147
+ * no conversation id, matching the dispatch path's own guard.
148
+ */
149
+ function buildNativeWebSearchProbeOptions(
150
+ callSite: LLMCallSite | undefined,
151
+ overrideProfile: string | undefined,
152
+ forceOverrideProfile: boolean,
153
+ conversationId: string | undefined,
154
+ ): SendMessageOptions | undefined {
155
+ if (!callSite) return undefined;
156
+ return {
157
+ config: {
158
+ callSite,
159
+ ...(overrideProfile ? { overrideProfile } : {}),
160
+ ...(overrideProfile && forceOverrideProfile
161
+ ? { forceOverrideProfile: true }
162
+ : {}),
163
+ ...(conversationId ? { selectionSeed: conversationId } : {}),
164
+ },
165
+ };
98
166
  }
99
167
 
100
168
  export interface CheckpointInfo {
@@ -625,6 +693,20 @@ export type LoopToolExecutor = (
625
693
  activityMetadata?: ToolActivityMetadata;
626
694
  }>;
627
695
 
696
+ /**
697
+ * The benign result returned for a sibling tool call that was deferred because
698
+ * an exclusive tool ran in the same turn. Phrased so the model treats it as a
699
+ * "not run yet" signal — read the exclusive tool's output, then re-issue this
700
+ * call if it is still the right next step.
701
+ */
702
+ function deferredForExclusiveMessage(exclusiveToolName: string): string {
703
+ return (
704
+ `(not run: \`${exclusiveToolName}\` was called this turn and runs first, on its own, ` +
705
+ `so the rest of your tool calls were held back. Read its output, then call this tool ` +
706
+ `again if it is still the right next step.)`
707
+ );
708
+ }
709
+
628
710
  export interface AgentLoopConstructorOptions {
629
711
  /** LLM provider the loop issues every call through. */
630
712
  provider: Provider;
@@ -634,6 +716,14 @@ export interface AgentLoopConstructorOptions {
634
716
  tools?: ToolDefinition[];
635
717
  toolExecutor?: LoopToolExecutor;
636
718
  resolveTools?: (history: Message[]) => ToolDefinition[];
719
+ /**
720
+ * Decide whether a tool runs exclusively in its turn (see
721
+ * {@link ToolDefinition.exclusive}). When it returns true for a tool present
722
+ * in a multi-call turn, the loop runs only that tool and defers the siblings
723
+ * un-run. Injected by the conversation wiring, which can read the tool
724
+ * registry; lightweight loops that omit it never defer.
725
+ */
726
+ isExclusiveTool?: (toolName: string) => boolean;
637
727
  /**
638
728
  * Conversation this loop drives. Scopes the loop-held compaction circuit
639
729
  * breaker and is the source of truth the loop's pipeline contexts and
@@ -659,6 +749,7 @@ export class AgentLoop {
659
749
  private tools: ToolDefinition[];
660
750
  private resolveTools: ((history: Message[]) => ToolDefinition[]) | null;
661
751
  private toolExecutor: LoopToolExecutor | null;
752
+ private isExclusiveTool: ((toolName: string) => boolean) | null;
662
753
 
663
754
  /**
664
755
  * Conversation this loop drives. Source of truth for the `conversationId`
@@ -688,6 +779,7 @@ export class AgentLoop {
688
779
  tools,
689
780
  toolExecutor,
690
781
  resolveTools,
782
+ isExclusiveTool,
691
783
  conversationId,
692
784
  resolveConversationDir,
693
785
  } = options;
@@ -697,6 +789,7 @@ export class AgentLoop {
697
789
  this.tools = tools ?? [];
698
790
  this.resolveTools = resolveTools ?? null;
699
791
  this.toolExecutor = toolExecutor ?? null;
792
+ this.isExclusiveTool = isExclusiveTool ?? null;
700
793
  this.conversationId = conversationId;
701
794
  this.resolveConversationDir = resolveConversationDir ?? null;
702
795
  this.compactionCircuit = new CompactionCircuit(this.conversationId);
@@ -1215,10 +1308,44 @@ export class AgentLoop {
1215
1308
 
1216
1309
  // Resolve tools for this turn: use the dynamic resolver if provided,
1217
1310
  // otherwise fall back to the static tool list.
1218
- const currentTools = this.resolveTools
1311
+ const resolvedTools = this.resolveTools
1219
1312
  ? this.resolveTools(history)
1220
1313
  : this.tools;
1221
1314
 
1315
+ // Provider-native web search: append a `web_search`-named tool that the
1316
+ // provider substitutes for its server-side search (run inline, no client
1317
+ // execution), gated STRICTLY on the capability of the provider/model
1318
+ // this call ACTUALLY routes to so a non-native provider never sees an
1319
+ // unexecutable client tool. The advisor consult's `advisorProfile` can
1320
+ // route `subagentSpawn` to a provider/model whose native-search support
1321
+ // differs from the construction-time default, so the gate resolves the
1322
+ // routed target (callSite + overrideProfile) via
1323
+ // `supportsNativeWebSearchFor` rather than the static
1324
+ // `this.provider.supportsNativeWebSearch` snapshot; providers without
1325
+ // the routing-aware probe fall back to the static flag. This is a SERVER
1326
+ // tool — it bypasses the client allowlist and the tool executor — so the
1327
+ // tool-less advisor consult can ground its guidance with live web access
1328
+ // while staying one-shot for client tools. Skip when a `web_search` tool
1329
+ // is already present so we never duplicate the name.
1330
+ const supportsRoutedNativeWebSearch = this.provider
1331
+ .supportsNativeWebSearchFor
1332
+ ? this.provider.supportsNativeWebSearchFor(
1333
+ buildNativeWebSearchProbeOptions(
1334
+ callSite,
1335
+ resolveEffectiveOverrideProfile(),
1336
+ forceOverrideProfile,
1337
+ this.conversationId,
1338
+ ),
1339
+ )
1340
+ : this.provider.supportsNativeWebSearch === true;
1341
+ const attachNativeWebSearch =
1342
+ this.config.enableNativeWebSearch === true &&
1343
+ supportsRoutedNativeWebSearch &&
1344
+ !resolvedTools.some((t) => t.name === NATIVE_WEB_SEARCH_TOOL.name);
1345
+ const currentTools = attachNativeWebSearch
1346
+ ? [...resolvedTools, NATIVE_WEB_SEARCH_TOOL]
1347
+ : resolvedTools;
1348
+
1222
1349
  // Field precedence (highest wins):
1223
1350
  // 1. Per-run explicit (`runModel`)
1224
1351
  // 2. Call-site resolved values (filled by
@@ -1261,6 +1388,11 @@ export class AgentLoop {
1261
1388
 
1262
1389
  if (this.config.toolChoice) {
1263
1390
  providerConfig.tool_choice = this.config.toolChoice;
1391
+ } else if (attachNativeWebSearch) {
1392
+ // The native web-search tool is the only tool on this turn (the
1393
+ // advisor consult is otherwise tool-less). Let the model decide
1394
+ // whether to search rather than forcing it.
1395
+ providerConfig.tool_choice = { type: "auto" };
1264
1396
  }
1265
1397
 
1266
1398
  if (this.config.cacheTtl) {
@@ -1883,8 +2015,39 @@ export class AgentLoop {
1883
2015
  "Tool execution start",
1884
2016
  );
1885
2017
 
2018
+ // When an exclusive tool (e.g. the advisor) is among this turn's calls,
2019
+ // it must run alone: the model should incorporate its output before
2020
+ // acting on anything else. Run only the first exclusive call and defer
2021
+ // the siblings with a benign, un-run result so the model re-issues them
2022
+ // next turn if still needed. Every tool_use still gets a matching
2023
+ // tool_result, so history stays well-formed.
2024
+ const exclusiveBlock = this.isExclusiveTool
2025
+ ? toolUseBlocks.find((block) => this.isExclusiveTool!(block.name))
2026
+ : undefined;
2027
+ const deferSiblings =
2028
+ exclusiveBlock !== undefined && toolUseBlocks.length > 1;
2029
+ if (deferSiblings) {
2030
+ rlog.info(
2031
+ {
2032
+ turn: toolUseTurns,
2033
+ exclusiveTool: exclusiveBlock!.name,
2034
+ deferred: toolUseBlocks
2035
+ .filter((block) => block !== exclusiveBlock)
2036
+ .map((block) => block.name),
2037
+ },
2038
+ "Exclusive tool present — running it alone and deferring sibling tool calls this turn",
2039
+ );
2040
+ }
2041
+
1886
2042
  const toolExecutionPromise = Promise.all(
1887
2043
  toolUseBlocks.map(async (toolUse) => {
2044
+ if (deferSiblings && toolUse !== exclusiveBlock) {
2045
+ const result: Awaited<ReturnType<LoopToolExecutor>> = {
2046
+ content: deferredForExclusiveMessage(exclusiveBlock!.name),
2047
+ isError: false,
2048
+ };
2049
+ return { toolUse, result };
2050
+ }
1888
2051
  const result = await this.toolExecutor!(
1889
2052
  toolUse.name,
1890
2053
  toolUse.input,
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Maximum number of events the daemon's per-process SSE replay ring
3
+ * retains for `Last-Event-ID` resume. This is the ring's count bound; the
4
+ * ring is also bounded by total bytes and entry age (whichever limit is
5
+ * hit first wins), so the live ring can hold *fewer* than this many
6
+ * events, never more. The daemon-side definition and eviction live in
7
+ * `assistant/src/runtime/assistant-stream-state.ts`.
8
+ *
9
+ * Exposed on the API surface so the web client's SSE consumer can size
10
+ * its seq-gap tolerance against the same number the daemon buffers
11
+ * against, instead of hard-coding a duplicate.
12
+ *
13
+ * A live seq gap smaller than this is benign: the global per-assistant
14
+ * `seq` counter is stamped before fanout, but the hub deliberately
15
+ * withholds some events from a given subscriber — self-echo-suppressed
16
+ * `sync_changed` (a client's own mutation echo) and capability-targeted
17
+ * host-proxy events — so a subscriber legitimately sees its cursor skip a
18
+ * few seqs it was never going to receive. Such a hole is not data loss
19
+ * and must not trigger a destructive authoritative snapshot heal. Only a
20
+ * gap that meets or exceeds this count proves the live suffix fell
21
+ * outside the ring entirely and is genuinely non-contiguous.
22
+ */
23
+ export const SSE_REPLAY_RING_COUNT_LIMIT = 200;
24
+
25
+ /**
26
+ * Maximum age (in milliseconds) an event may reach in the daemon's SSE
27
+ * replay ring before it is evicted, regardless of count or byte usage.
28
+ * The daemon-side definition and eviction live in
29
+ * `assistant/src/runtime/assistant-stream-state.ts`.
30
+ *
31
+ * Exposed alongside the count bound so the web client can reason about
32
+ * the age dimension of the ring too. A small live seq gap is only safe to
33
+ * treat as benign when the client has been continuously receiving events
34
+ * — live delivery is concurrent with stamping, so a connected subscriber
35
+ * never relies on the ring. Once the connection has been quiet for longer
36
+ * than this window (a disconnect/resume), events the client missed may
37
+ * have aged out of the ring and become unrecoverable by replay, so even a
38
+ * small seq gap must trigger an authoritative reconcile rather than be
39
+ * waved through.
40
+ */
41
+ export const SSE_REPLAY_RING_AGE_LIMIT_MS = 30_000;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * `conversation_notice` SSE event.
3
+ *
4
+ * Non-terminal, conversation-scoped notice for actionable runtime conditions
5
+ * that should not mark the turn as failed. The client may render CTA UI from
6
+ * this event while preserving the current assistant response.
7
+ */
8
+
9
+ import { z } from "zod";
10
+
11
+ import { ConversationErrorCodeSchema } from "./conversation-error.js";
12
+
13
+ export const ConversationNoticeSourceSchema = z.enum(["memory_v3"]);
14
+
15
+ export const ConversationNoticeEventSchema = z.object({
16
+ type: z.literal("conversation_notice"),
17
+ conversationId: z.string(),
18
+ source: ConversationNoticeSourceSchema,
19
+ code: ConversationErrorCodeSchema,
20
+ userMessage: z.string(),
21
+ errorCategory: z.string().optional(),
22
+ });
23
+
24
+ export type ConversationNoticeEvent = z.infer<
25
+ typeof ConversationNoticeEventSchema
26
+ >;
package/src/api/index.ts CHANGED
@@ -11,6 +11,7 @@ import { ConfirmationRequestEventSchema } from "./events/confirmation-request.js
11
11
  import { ContactRequestEventSchema } from "./events/contact-request.js";
12
12
  import { ConversationErrorEventSchema } from "./events/conversation-error.js";
13
13
  import { ConversationListInvalidatedEventSchema } from "./events/conversation-list-invalidated.js";
14
+ import { ConversationNoticeEventSchema } from "./events/conversation-notice.js";
14
15
  import { ConversationTitleUpdatedEventSchema } from "./events/conversation-title-updated.js";
15
16
  import { DiskPressureStatusChangedEventSchema } from "./events/disk-pressure-status-changed.js";
16
17
  import { DocumentCommentCreatedEventSchema } from "./events/document-comment-created.js";
@@ -61,6 +62,10 @@ export {
61
62
  CALL_SITE_COMPACTION_AGENT,
62
63
  CALL_SITE_SYNTHETIC_AGENT_ERROR_MESSAGE,
63
64
  } from "./constants/call-sites.js";
65
+ export {
66
+ SSE_REPLAY_RING_AGE_LIMIT_MS,
67
+ SSE_REPLAY_RING_COUNT_LIMIT,
68
+ } from "./constants/sse-replay.js";
64
69
  export { DEFAULT_TOOL_EXECUTION_TIMEOUT_SEC } from "./constants/tool-execution.js";
65
70
  export {
66
71
  type AssistantActivityAnchor,
@@ -134,6 +139,11 @@ export {
134
139
  type ConversationListInvalidatedReason,
135
140
  ConversationListInvalidatedReasonSchema,
136
141
  } from "./events/conversation-list-invalidated.js";
142
+ export {
143
+ type ConversationNoticeEvent,
144
+ ConversationNoticeEventSchema,
145
+ ConversationNoticeSourceSchema,
146
+ } from "./events/conversation-notice.js";
137
147
  export {
138
148
  type ConversationTitleUpdatedEvent,
139
149
  ConversationTitleUpdatedEventSchema,
@@ -430,6 +440,8 @@ export {
430
440
  LlmContextResponseSchema,
431
441
  } from "./responses/llm-context-response.js";
432
442
  export {
443
+ type LLMCallError,
444
+ LLMCallErrorSchema,
433
445
  type LLMCallSummary,
434
446
  LLMCallSummarySchema,
435
447
  type LLMContextSection,
@@ -471,7 +483,12 @@ export {
471
483
  type WorkflowLeaf,
472
484
  WorkflowLeafSchema,
473
485
  } from "./responses/workflow-journal.js";
474
- export { type CardSurfaceData, CardSurfaceDataSchema } from "./surfaces.js";
486
+ export {
487
+ type CardSurfaceData,
488
+ CardSurfaceDataSchema,
489
+ type FileUploadSurfaceData,
490
+ FileUploadSurfaceDataSchema,
491
+ } from "./surfaces.js";
475
492
 
476
493
  /**
477
494
  * Canonical SSE event schema for the assistant runtime.
@@ -498,6 +515,7 @@ export const AssistantEventSchema = z.discriminatedUnion("type", [
498
515
  ContactRequestEventSchema,
499
516
  ConversationErrorEventSchema,
500
517
  ConversationListInvalidatedEventSchema,
518
+ ConversationNoticeEventSchema,
501
519
  ConversationTitleUpdatedEventSchema,
502
520
  DiskPressureStatusChangedEventSchema,
503
521
  DocumentCommentCreatedEventSchema,
@@ -67,6 +67,34 @@ export const LLMContextSectionSchema = z.object({
67
67
 
68
68
  export type LLMContextSection = z.infer<typeof LLMContextSectionSchema>;
69
69
 
70
+ /**
71
+ * Structured provider/transport error recorded when an LLM call was
72
+ * rejected before producing a response. Mirrors the on-disk
73
+ * `responsePayload.error` shape written by
74
+ * `buildProviderErrorResponsePayload` — the inspector branches on the
75
+ * presence of this field to render a failed call distinctly (failure
76
+ * banner in the Response tab, $0.00 cost in the rail, etc.) instead of
77
+ * the generic "section rendering unavailable" fallback.
78
+ *
79
+ * Every field is optional because the serializer degrades a plain
80
+ * `Error` down to just `{ name, message }`; only the wrapper object is
81
+ * guaranteed.
82
+ */
83
+ export const LLMCallErrorSchema = z.object({
84
+ name: z.string().nullish(),
85
+ message: z.string().nullish(),
86
+ code: z.string().nullish(),
87
+ provider: z.string().nullish(),
88
+ statusCode: z.number().nullish(),
89
+ retryAfterMs: z.number().nullish(),
90
+ apiErrorCode: z.string().nullish(),
91
+ apiErrorType: z.string().nullish(),
92
+ apiErrorParam: z.string().nullish(),
93
+ requestId: z.string().nullish(),
94
+ });
95
+
96
+ export type LLMCallError = z.infer<typeof LLMCallErrorSchema>;
97
+
70
98
  /**
71
99
  * One LLM request log row.
72
100
  *
@@ -88,6 +116,7 @@ export const LLMRequestLogEntrySchema = z.object({
88
116
  responseSections: z.array(LLMContextSectionSchema).nullish(),
89
117
  agentLoopExitReason: z.string().nullish(),
90
118
  callSite: z.string().nullish(),
119
+ error: LLMCallErrorSchema.nullish(),
91
120
  });
92
121
 
93
122
  export type LLMRequestLogEntry = z.infer<typeof LLMRequestLogEntrySchema>;
@@ -31,6 +31,23 @@ export const SubagentDetailEventSchema = z.object({
31
31
  toolName: z.string().optional(),
32
32
  isError: z.boolean().optional(),
33
33
  messageId: z.string().optional(),
34
+ /**
35
+ * Tool-call id — the `tool_use.id` on a tool-call event and the referencing
36
+ * `tool_use_id` on its tool-result event, in the daemon's canonical
37
+ * content-block format. That format is provider-agnostic: every provider
38
+ * (Anthropic, OpenAI, Gemini, …) normalizes its native tool calls into these
39
+ * `tool_use`/`tool_result` blocks (see `providers/types.ts`), so this id is
40
+ * present regardless of which model produced the call. Lets the web client
41
+ * pair a result with its call and key the nested tool-detail view, so tool
42
+ * pills on reloaded/history subagents are clickable (not just live ones).
43
+ */
44
+ toolUseId: z.string().optional(),
45
+ /**
46
+ * Raw tool input object on tool-call events. (`content` also carries a
47
+ * JSON-stringified copy for back-compat / label derivation.) Surfaced in the
48
+ * tool-detail view's input section.
49
+ */
50
+ input: z.record(z.string(), z.unknown()).optional(),
34
51
  });
35
52
 
36
53
  export type SubagentDetailEvent = z.infer<typeof SubagentDetailEventSchema>;
@@ -11,9 +11,9 @@
11
11
  * normalizer *supports* — anything the model sends outside these fields is
12
12
  * dropped (and logged) there, which is how we learn the shapes to recover.
13
13
  *
14
- * Card is the first surface type migrated to a canonical schema; the remaining
15
- * types still live as hand-written interfaces in
16
- * `daemon/message-types/surfaces.ts` pending migration.
14
+ * Card and file_upload use canonical schemas; the remaining types are
15
+ * hand-written interfaces in `daemon/message-types/surfaces.ts` pending
16
+ * migration.
17
17
  */
18
18
 
19
19
  import { z } from "zod";
@@ -31,3 +31,39 @@ export const CardSurfaceDataSchema = z.object({
31
31
  templateData: z.record(z.string(), z.unknown()).optional(),
32
32
  });
33
33
  export type CardSurfaceData = z.infer<typeof CardSurfaceDataSchema>;
34
+
35
+ /**
36
+ * Accepted MIME-type / extension patterns for a `file_upload` surface.
37
+ *
38
+ * The renderer consumes this as a `string[]` — it calls `.join`/`.some`/
39
+ * `.length` on the value — but the model may emit a single comma-joined string
40
+ * ("image/*, application/pdf") or a bare string. Coercing every shape to a
41
+ * clean `string[]` keeps that array invariant intact: a string is split on
42
+ * commas; array entries are stringified and trimmed; blanks and any non-array
43
+ * value collapse to `undefined` (no restriction).
44
+ */
45
+ const FileUploadAcceptedTypesSchema = z.preprocess((value) => {
46
+ const items =
47
+ typeof value === "string"
48
+ ? value.split(",")
49
+ : Array.isArray(value)
50
+ ? value
51
+ : [];
52
+ const cleaned = items
53
+ .map((item) =>
54
+ typeof item === "string" || typeof item === "number"
55
+ ? String(item).trim()
56
+ : "",
57
+ )
58
+ .filter((item) => item.length > 0);
59
+ return cleaned.length > 0 ? cleaned : undefined;
60
+ }, z.array(z.string()).optional());
61
+
62
+ export const FileUploadSurfaceDataSchema = z.object({
63
+ prompt: z.coerce.string().optional(),
64
+ acceptedTypes: FileUploadAcceptedTypesSchema,
65
+ /** A non-positive or non-numeric value is dropped rather than rejecting the surface. */
66
+ maxFiles: z.coerce.number().int().positive().optional().catch(undefined),
67
+ maxSizeBytes: z.coerce.number().positive().optional().catch(undefined),
68
+ });
69
+ export type FileUploadSurfaceData = z.infer<typeof FileUploadSurfaceDataSchema>;