@vellumai/assistant 0.8.4 → 0.8.5

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 (438) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/docs/browser-use-architecture-phase2.md +1 -1
  3. package/knip.json +2 -1
  4. package/openapi.yaml +809 -11
  5. package/package.json +1 -1
  6. package/src/__tests__/anthropic-provider.test.ts +34 -37
  7. package/src/__tests__/assistant-event-hub-self-exclusion.test.ts +293 -0
  8. package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -3
  9. package/src/__tests__/audit-log-rotation.test.ts +70 -16
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +3 -3
  11. package/src/__tests__/btw-routes.test.ts +2 -3
  12. package/src/__tests__/call-controller.test.ts +0 -1
  13. package/src/__tests__/cancel-resolves-conversation-key.test.ts +1 -1
  14. package/src/__tests__/channel-guardian.test.ts +3 -3
  15. package/src/__tests__/checker.test.ts +6 -15
  16. package/src/__tests__/compaction-events.test.ts +1 -0
  17. package/src/__tests__/compactor-call-site-logging.test.ts +214 -0
  18. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +5 -11
  19. package/src/__tests__/computer-use-tools.test.ts +2 -4
  20. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  21. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +1 -1
  22. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  23. package/src/__tests__/conversation-agent-loop-overflow.test.ts +197 -2
  24. package/src/__tests__/conversation-agent-loop.test.ts +163 -122
  25. package/src/__tests__/conversation-app-control-instantiation.test.ts +2 -5
  26. package/src/__tests__/conversation-clear-safety.test.ts +25 -25
  27. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +1 -1
  28. package/src/__tests__/conversation-disk-view-integration.test.ts +2 -2
  29. package/src/__tests__/conversation-error.test.ts +31 -0
  30. package/src/__tests__/conversation-fork-crud.test.ts +178 -15
  31. package/src/__tests__/conversation-lifecycle.test.ts +52 -11
  32. package/src/__tests__/{conversation-load-cleaned-at.test.ts → conversation-load-history-stripped.test.ts} +13 -13
  33. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -0
  34. package/src/__tests__/conversation-routes-disk-view.test.ts +109 -0
  35. package/src/__tests__/conversation-routes-slash-commands.test.ts +35 -0
  36. package/src/__tests__/conversation-skill-tools.test.ts +2 -5
  37. package/src/__tests__/conversation-store.test.ts +1 -1
  38. package/src/__tests__/conversation-sync-tags.test.ts +99 -32
  39. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  40. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  42. package/src/__tests__/credential-execution-feature-gates.test.ts +9 -7
  43. package/src/__tests__/credential-execution-tools.test.ts +6 -6
  44. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  45. package/src/__tests__/credential-vault-unit.test.ts +2 -2
  46. package/src/__tests__/dynamic-page-surface.test.ts +2 -2
  47. package/src/__tests__/email-html-renderer.test.ts +12 -0
  48. package/src/__tests__/gateway-flag-listener.test.ts +237 -0
  49. package/src/__tests__/gemini-provider.test.ts +78 -0
  50. package/src/__tests__/guardian-dispatch.test.ts +0 -1
  51. package/src/__tests__/guardian-outbound-http.test.ts +7 -5
  52. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  53. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -0
  54. package/src/__tests__/heartbeat-service.test.ts +4 -0
  55. package/src/__tests__/host-shell-tool.test.ts +1 -1
  56. package/src/__tests__/init-feature-flag-overrides.test.ts +5 -6
  57. package/src/__tests__/list-messages-tool-merge.test.ts +70 -11
  58. package/src/__tests__/llm-request-log-call-site.test.ts +136 -0
  59. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +26 -0
  60. package/src/__tests__/llm-resolver.test.ts +77 -9
  61. package/src/__tests__/llm-usage-store.test.ts +66 -0
  62. package/src/__tests__/logger.test.ts +89 -0
  63. package/src/__tests__/mcp-abort-signal.test.ts +2 -2
  64. package/src/__tests__/media-generate-image.test.ts +31 -0
  65. package/src/__tests__/memory-v2-static-injector.test.ts +7 -7
  66. package/src/__tests__/model-intents.test.ts +2 -4
  67. package/src/__tests__/notification-guardian-path.test.ts +0 -1
  68. package/src/__tests__/onboarding-template-contract.test.ts +1 -1
  69. package/src/__tests__/openai-provider.test.ts +46 -0
  70. package/src/__tests__/openai-responses-provider.test.ts +114 -12
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +0 -1
  72. package/src/__tests__/platform-bash-auto-approve.test.ts +2 -2
  73. package/src/__tests__/platform.test.ts +2 -2
  74. package/src/__tests__/plugin-api-tool-definition.test.ts +92 -0
  75. package/src/__tests__/plugin-bootstrap.test.ts +2 -2
  76. package/src/__tests__/plugin-tool-contribution.test.ts +13 -6
  77. package/src/__tests__/plugin-types.test.ts +3 -2
  78. package/src/__tests__/prechat-onboarding-contract.test.ts +131 -98
  79. package/src/__tests__/pricing.test.ts +12 -0
  80. package/src/__tests__/prune-jobs-changes-parser.test.ts +61 -0
  81. package/src/__tests__/registry.test.ts +2 -8
  82. package/src/__tests__/require-fresh-approval.test.ts +2 -2
  83. package/src/__tests__/runtime-events-sse-bilingual.test.ts +154 -0
  84. package/src/__tests__/shell-tool-proxy-mode.test.ts +1 -1
  85. package/src/__tests__/skill-feature-flags.test.ts +2 -2
  86. package/src/__tests__/skill-projection-feature-flag.test.ts +4 -7
  87. package/src/__tests__/skill-projection.benchmark.test.ts +2 -6
  88. package/src/__tests__/skill-tool-factory.test.ts +1 -1
  89. package/src/__tests__/subagent-notify-parent.test.ts +1 -1
  90. package/src/__tests__/suggestion-routes.test.ts +1 -0
  91. package/src/__tests__/sync-message-contract.test.ts +59 -0
  92. package/src/__tests__/system-prompt.test.ts +145 -131
  93. package/src/__tests__/terminal-tools.test.ts +1 -1
  94. package/src/__tests__/tool-approval-handler.test.ts +1 -5
  95. package/src/__tests__/tool-execute-pipeline.test.ts +2 -2
  96. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -5
  97. package/src/__tests__/tool-executor-lifecycle-events.test.ts +15 -5
  98. package/src/__tests__/tool-executor.test.ts +9 -62
  99. package/src/__tests__/tool-grant-request-escalation.test.ts +1 -6
  100. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  101. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -6
  102. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -1
  103. package/src/__tests__/ui-file-upload-surface.test.ts +2 -2
  104. package/src/__tests__/usage-routes.test.ts +3 -0
  105. package/src/__tests__/verification-control-plane-policy.test.ts +2 -2
  106. package/src/__tests__/workspace-git-service.test.ts +6 -5
  107. package/src/__tests__/workspace-migration-089-move-memory-tree-out-of-v3.test.ts +86 -0
  108. package/src/acp/__tests__/prepare-agent-env.test.ts +146 -0
  109. package/src/acp/prepare-agent-env.ts +78 -0
  110. package/src/acp/session-manager.ts +1 -1
  111. package/src/agent/loop.ts +8 -0
  112. package/src/api/README.md +5 -0
  113. package/src/api/index.ts +4 -0
  114. package/src/api/package.json +10 -0
  115. package/src/background-wake/background-wake-routes.test.ts +233 -0
  116. package/src/background-wake/runtime-registry.ts +24 -0
  117. package/src/cli/commands/__tests__/browser.test.ts +23 -5
  118. package/src/cli/commands/__tests__/domain-register.test.ts +110 -0
  119. package/src/cli/commands/__tests__/domain-status.test.ts +33 -33
  120. package/src/cli/commands/__tests__/inference-send.test.ts +108 -5
  121. package/src/cli/commands/__tests__/memory-v2-compare-render.test.ts +98 -0
  122. package/src/cli/commands/__tests__/memory-v2.test.ts +1 -0
  123. package/src/cli/commands/__tests__/memory-v3-render.test.ts +340 -0
  124. package/src/cli/commands/browser.ts +247 -0
  125. package/src/cli/commands/domain.ts +91 -41
  126. package/src/cli/commands/inference.ts +93 -40
  127. package/src/cli/commands/memory-v2-compare-render.ts +115 -0
  128. package/src/cli/commands/memory-v2.ts +176 -1
  129. package/src/cli/commands/memory-v3-render.ts +344 -0
  130. package/src/cli/commands/memory-v3.ts +316 -0
  131. package/src/cli/program.ts +2 -0
  132. package/src/config/assistant-feature-flags.ts +21 -9
  133. package/src/config/bundled-skills/document-editor/SKILL.md +11 -2
  134. package/src/config/bundled-skills/document-editor/TOOLS.json +18 -0
  135. package/src/config/bundled-skills/document-editor/tools/document-open.ts +12 -0
  136. package/src/config/bundled-skills/image-studio/SKILL.md +4 -0
  137. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -2
  138. package/src/config/bundled-skills/media-processing/tools/ingest-media.ts +13 -8
  139. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +10 -3
  140. package/src/config/bundled-skills/phone-calls/references/TRANSCRIPTS.md +16 -14
  141. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +7 -2
  142. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +7 -2
  143. package/src/config/bundled-tool-registry.ts +2 -0
  144. package/src/config/call-site-defaults.ts +7 -6
  145. package/src/config/feature-flag-registry.json +16 -0
  146. package/src/config/schemas/__tests__/memory-v2.test.ts +213 -1
  147. package/src/config/schemas/call-site-catalog.ts +21 -7
  148. package/src/config/schemas/llm.ts +12 -1
  149. package/src/config/schemas/memory-v2.ts +246 -0
  150. package/src/config/schemas/memory.ts +2 -1
  151. package/src/context/compactor.ts +52 -0
  152. package/src/conversations/__tests__/message-consolidation.test.ts +350 -0
  153. package/src/conversations/message-consolidation.ts +404 -0
  154. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +1 -1
  155. package/src/daemon/__tests__/meet-manifest-loader.test.ts +1 -1
  156. package/src/daemon/conversation-agent-loop-handlers.ts +2 -13
  157. package/src/daemon/conversation-agent-loop.ts +126 -76
  158. package/src/daemon/conversation-error.ts +31 -1
  159. package/src/daemon/conversation-lifecycle.ts +27 -22
  160. package/src/daemon/conversation-runtime-assembly.ts +10 -9
  161. package/src/daemon/conversation-tool-setup.ts +63 -3
  162. package/src/daemon/conversation-usage.ts +2 -0
  163. package/src/daemon/conversation.ts +14 -29
  164. package/src/daemon/disk-pressure-guard.ts +14 -2
  165. package/src/daemon/handlers/config-model.test.ts +1 -0
  166. package/src/daemon/handlers/conversations.ts +11 -3
  167. package/src/daemon/host-browser-proxy.ts +5 -5
  168. package/src/daemon/host-cu-proxy.ts +4 -4
  169. package/src/daemon/host-file-proxy.ts +4 -4
  170. package/src/daemon/host-proxy-base.ts +4 -4
  171. package/src/daemon/host-transfer-proxy.ts +10 -10
  172. package/src/daemon/lifecycle.ts +23 -20
  173. package/src/daemon/meet-manifest-loader.ts +1 -7
  174. package/src/daemon/message-types/conversations.ts +6 -9
  175. package/src/daemon/message-types/home.ts +1 -13
  176. package/src/daemon/message-types/messages.ts +6 -14
  177. package/src/daemon/message-types/sync.ts +14 -0
  178. package/src/daemon/shutdown-handlers.ts +24 -5
  179. package/src/daemon/switch-inference-profile-tool.ts +52 -0
  180. package/src/daemon/tool-setup-types.ts +13 -0
  181. package/src/events/relationship-state-updated.ts +25 -0
  182. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  183. package/src/home/home-greeting.ts +0 -9
  184. package/src/home/suggested-prompts.ts +0 -9
  185. package/src/ipc/gateway-flag-listener.ts +123 -0
  186. package/src/ipc/skill-routes/registries.ts +8 -12
  187. package/src/memory/__tests__/db-async-query.test.ts +165 -0
  188. package/src/memory/__tests__/db-maintenance.test.ts +115 -0
  189. package/src/memory/__tests__/jobs-store-enqueue-gate.test.ts +241 -0
  190. package/src/memory/__tests__/jobs-store-job-classes.test.ts +28 -1
  191. package/src/memory/__tests__/memory-retrospective-job.test.ts +7 -0
  192. package/src/memory/auto-analysis-enqueue.ts +5 -1
  193. package/src/memory/conversation-crud.ts +71 -70
  194. package/src/memory/conversation-starters-cadence.ts +3 -1
  195. package/src/memory/conversation-title-service.ts +19 -3
  196. package/src/memory/db-async-query.ts +214 -0
  197. package/src/memory/db-init.ts +10 -0
  198. package/src/memory/db-maintenance.ts +30 -21
  199. package/src/memory/graph/bootstrap.ts +8 -1
  200. package/src/memory/graph/capability-seed.ts +7 -3
  201. package/src/memory/graph/conversation-graph-memory.ts +100 -17
  202. package/src/memory/graph/extraction.ts +1 -5
  203. package/src/memory/graph/graph-search.ts +7 -1
  204. package/src/memory/indexer.ts +28 -18
  205. package/src/memory/job-handlers/cleanup.ts +76 -18
  206. package/src/memory/job-handlers/conversation-starters.ts +1 -4
  207. package/src/memory/jobs/embed-pkb-file.ts +6 -1
  208. package/src/memory/jobs-store.ts +14 -0
  209. package/src/memory/jobs-worker.ts +55 -22
  210. package/src/memory/llm-request-log-source-clickhouse.ts +42 -2
  211. package/src/memory/llm-request-log-source-local.ts +7 -0
  212. package/src/memory/llm-request-log-source.ts +9 -2
  213. package/src/memory/llm-request-log-store.ts +43 -1
  214. package/src/memory/llm-usage-store.ts +24 -0
  215. package/src/memory/memory-retrospective-enqueue.ts +8 -1
  216. package/src/memory/memory-retrospective-job.ts +5 -0
  217. package/src/memory/memory-v2-activation-log-store.ts +15 -6
  218. package/src/memory/migrations/260-rename-cleaned-at.ts +44 -0
  219. package/src/memory/migrations/261-llm-usage-add-raw-usage.ts +36 -0
  220. package/src/memory/migrations/262-memory-v3-coactivation.ts +57 -0
  221. package/src/memory/migrations/263-memory-v3-auto-edges.ts +50 -0
  222. package/src/memory/migrations/264-llm-request-log-call-site.ts +29 -0
  223. package/src/memory/migrations/index.ts +17 -0
  224. package/src/memory/migrations/registry.ts +33 -0
  225. package/src/memory/schema/conversations.ts +1 -1
  226. package/src/memory/schema/infrastructure.ts +21 -0
  227. package/src/memory/tool-usage-store.ts +36 -8
  228. package/src/memory/v2/__tests__/consolidation-job.test.ts +1 -0
  229. package/src/memory/v2/__tests__/harness-compare.test.ts +186 -0
  230. package/src/memory/v2/__tests__/harness-metrics.test.ts +74 -0
  231. package/src/memory/v2/__tests__/harness-oracle.test.ts +257 -0
  232. package/src/memory/v2/__tests__/harness-replay-input.test.ts +225 -0
  233. package/src/memory/v2/__tests__/harness-runner.test.ts +109 -0
  234. package/src/memory/v2/__tests__/injection.test.ts +127 -98
  235. package/src/memory/v2/__tests__/qdrant.test.ts +36 -0
  236. package/src/memory/v2/__tests__/router.test.ts +171 -3
  237. package/src/memory/v2/harness/compare.ts +57 -0
  238. package/src/memory/v2/harness/metrics.ts +124 -0
  239. package/src/memory/v2/harness/oracle.ts +145 -0
  240. package/src/memory/v2/harness/replay-input.ts +224 -0
  241. package/src/memory/v2/harness/retriever.ts +74 -0
  242. package/src/memory/v2/harness/router-retriever.ts +43 -0
  243. package/src/memory/v2/harness/runner.ts +106 -0
  244. package/src/memory/v2/harness/trace.ts +58 -0
  245. package/src/memory/v2/injection.ts +21 -15
  246. package/src/memory/v2/prompts/router.ts +26 -1
  247. package/src/memory/v2/qdrant.ts +14 -2
  248. package/src/memory/v2/router.ts +171 -18
  249. package/src/memory/v3/__tests__/coactivation-store.test.ts +422 -0
  250. package/src/memory/v3/__tests__/consolidation-job.test.ts +468 -0
  251. package/src/memory/v3/__tests__/edge-learning-job.test.ts +324 -0
  252. package/src/memory/v3/__tests__/edges.test.ts +563 -0
  253. package/src/memory/v3/__tests__/filter.test.ts +512 -0
  254. package/src/memory/v3/__tests__/gate.test.ts +574 -0
  255. package/src/memory/v3/__tests__/index-composition.test.ts +233 -0
  256. package/src/memory/v3/__tests__/loop.test.ts +530 -0
  257. package/src/memory/v3/__tests__/retriever.test.ts +226 -0
  258. package/src/memory/v3/__tests__/scouts.test.ts +440 -0
  259. package/src/memory/v3/__tests__/shadow-middleware.test.ts +312 -0
  260. package/src/memory/v3/__tests__/system-prompts.test.ts +154 -0
  261. package/src/memory/v3/__tests__/traversal.test.ts +469 -0
  262. package/src/memory/v3/__tests__/tree-index.test.ts +280 -0
  263. package/src/memory/v3/__tests__/tree-store.test.ts +529 -0
  264. package/src/memory/v3/__tests__/tree-walk.test.ts +707 -0
  265. package/src/memory/v3/__tests__/validate.test.ts +245 -0
  266. package/src/memory/v3/auto-edges.ts +223 -0
  267. package/src/memory/v3/coactivation-store.ts +124 -0
  268. package/src/memory/v3/consolidation-job.ts +323 -0
  269. package/src/memory/v3/edge-learning-job.ts +160 -0
  270. package/src/memory/v3/edges.ts +249 -0
  271. package/src/memory/v3/filter.ts +281 -0
  272. package/src/memory/v3/gate.ts +334 -0
  273. package/src/memory/v3/index-composition.ts +113 -0
  274. package/src/memory/v3/llm-capture.ts +46 -0
  275. package/src/memory/v3/loop.ts +382 -0
  276. package/src/memory/v3/maintenance.ts +144 -0
  277. package/src/memory/v3/prompt-context.ts +33 -0
  278. package/src/memory/v3/prompts/consolidation.ts +458 -0
  279. package/src/memory/v3/prompts/system-prompts.ts +196 -0
  280. package/src/memory/v3/retriever.ts +33 -0
  281. package/src/memory/v3/scouts.ts +420 -0
  282. package/src/memory/v3/shadow-middleware.ts +305 -0
  283. package/src/memory/v3/traversal.ts +206 -0
  284. package/src/memory/v3/tree-index.ts +237 -0
  285. package/src/memory/v3/tree-store.ts +394 -0
  286. package/src/memory/v3/tree-walk.ts +351 -0
  287. package/src/memory/v3/types.ts +65 -0
  288. package/src/memory/v3/validate.ts +300 -0
  289. package/src/notifications/adapters/macos.ts +18 -1
  290. package/src/notifications/adapters/platform.ts +1 -1
  291. package/src/notifications/decision-engine.ts +1 -4
  292. package/src/notifications/emit-signal.ts +29 -49
  293. package/src/permissions/prompter.ts +3 -3
  294. package/src/permissions/question-prompter.ts +5 -2
  295. package/src/permissions/secret-prompter.ts +2 -2
  296. package/src/plugin-api/index.ts +4 -0
  297. package/src/plugin-api/types.ts +7 -33
  298. package/src/plugins/defaults/index.ts +6 -0
  299. package/src/plugins/defaults/injectors.ts +18 -11
  300. package/src/plugins/external-plugin-loader.ts +5 -68
  301. package/src/plugins/types.ts +11 -16
  302. package/src/proactive-artifact/aux-message-injector.ts +17 -4
  303. package/src/prompts/__tests__/task-progress-hint-section.test.ts +3 -9
  304. package/src/prompts/persona-resolver.ts +36 -21
  305. package/src/prompts/sections.ts +39 -7
  306. package/src/prompts/system-prompt.ts +50 -185
  307. package/src/prompts/templates/BOOTSTRAP.md +2 -2
  308. package/src/prompts/templates/system-sections.ts +230 -8
  309. package/src/providers/__tests__/connection-model-compat.test.ts +234 -0
  310. package/src/providers/__tests__/retry-callsite.test.ts +85 -5
  311. package/src/providers/anthropic/client.ts +32 -66
  312. package/src/providers/call-site-routing.ts +14 -2
  313. package/src/providers/connection-model-compat.ts +38 -0
  314. package/src/providers/connection-resolution.ts +16 -2
  315. package/src/providers/gemini/client.ts +49 -6
  316. package/src/providers/inference/adapter-factory.ts +3 -0
  317. package/src/providers/minimax/client.ts +106 -0
  318. package/src/providers/model-catalog.ts +43 -0
  319. package/src/providers/model-intents.ts +1 -1
  320. package/src/providers/openai/chat-completions-provider.ts +6 -3
  321. package/src/providers/openai/codex-models.ts +18 -0
  322. package/src/providers/openai/responses-provider.ts +78 -21
  323. package/src/providers/provider-send-message.ts +7 -1
  324. package/src/providers/retry.ts +34 -3
  325. package/src/providers/thinking-config.ts +26 -1
  326. package/src/providers/usage-tracking.ts +2 -0
  327. package/src/runtime/AGENTS.md +2 -2
  328. package/src/runtime/agent-wake.ts +1 -0
  329. package/src/runtime/assistant-event-hub.ts +76 -6
  330. package/src/runtime/auth/route-policy.ts +36 -0
  331. package/src/runtime/btw-sidechain.ts +0 -6
  332. package/src/runtime/http-types.ts +0 -2
  333. package/src/runtime/migrations/vbundle-builder.ts +10 -3
  334. package/src/runtime/pending-interactions.ts +0 -1
  335. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +106 -0
  336. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +25 -6
  337. package/src/runtime/routes/__tests__/plugins-routes.test.ts +512 -0
  338. package/src/runtime/routes/acp-routes.test.ts +255 -6
  339. package/src/runtime/routes/acp-routes.ts +8 -1
  340. package/src/runtime/routes/avatar-routes.ts +10 -10
  341. package/src/runtime/routes/background-wake-routes.ts +188 -0
  342. package/src/runtime/routes/browser-tabs-routes.ts +200 -0
  343. package/src/runtime/routes/btw-routes.ts +0 -6
  344. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  345. package/src/runtime/routes/conversation-list-routes.ts +12 -4
  346. package/src/runtime/routes/conversation-management-routes.ts +77 -20
  347. package/src/runtime/routes/conversation-query-routes.ts +142 -36
  348. package/src/runtime/routes/conversation-routes.ts +252 -410
  349. package/src/runtime/routes/conversation-starter-routes.ts +6 -3
  350. package/src/runtime/routes/disk-pressure-routes.ts +1 -1
  351. package/src/runtime/routes/domain-routes.ts +60 -10
  352. package/src/runtime/routes/email-routes.ts +5 -2
  353. package/src/runtime/routes/events-routes.ts +54 -10
  354. package/src/runtime/routes/group-routes.ts +24 -8
  355. package/src/runtime/routes/host-browser-routes.ts +10 -2
  356. package/src/runtime/routes/host-cu-routes.ts +2 -2
  357. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +96 -3
  358. package/src/runtime/routes/index.ts +8 -0
  359. package/src/runtime/routes/inference-profile-session-handler.ts +22 -12
  360. package/src/runtime/routes/inference-profile-session-routes.ts +7 -1
  361. package/src/runtime/routes/llm-call-sites-routes.ts +32 -5
  362. package/src/runtime/routes/memory-item-routes.ts +8 -3
  363. package/src/runtime/routes/memory-v2-routes.ts +215 -5
  364. package/src/runtime/routes/memory-v3-routes.ts +316 -0
  365. package/src/runtime/routes/migration-routes.ts +21 -24
  366. package/src/runtime/routes/plugins-routes.ts +337 -0
  367. package/src/runtime/routes/rename-conversation-routes.ts +6 -2
  368. package/src/runtime/routes/secret-routes.ts +25 -5
  369. package/src/runtime/routes/settings-routes.ts +12 -11
  370. package/src/runtime/routes/slack-channel-routes.ts +5 -4
  371. package/src/runtime/routes/workspace-routes.ts +25 -10
  372. package/src/runtime/sync/resource-sync-events.ts +106 -38
  373. package/src/runtime/sync/sync-publisher.test.ts +49 -0
  374. package/src/runtime/sync/sync-publisher.ts +2 -1
  375. package/src/runtime/verification-outbound-actions.ts +73 -1
  376. package/src/telemetry/types.ts +12 -0
  377. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  378. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  379. package/src/tools/acp/spawn.test.ts +119 -0
  380. package/src/tools/acp/spawn.ts +15 -2
  381. package/src/tools/apps/definitions.ts +2 -8
  382. package/src/tools/ask-question/ask-question-tool.test.ts +3 -3
  383. package/src/tools/ask-question/ask-question-tool.ts +38 -45
  384. package/src/tools/browser/__tests__/pinned-tabs.test.ts +70 -0
  385. package/src/tools/browser/browser-execution.ts +16 -3
  386. package/src/tools/browser/cdp-client/__tests__/browser-tabs-factory.test.ts +402 -0
  387. package/src/tools/browser/cdp-client/__tests__/types.test.ts +3 -0
  388. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +12 -0
  389. package/src/tools/browser/cdp-client/extension-cdp-client.ts +27 -1
  390. package/src/tools/browser/cdp-client/factory.ts +100 -17
  391. package/src/tools/browser/cdp-client/local-cdp-client.ts +12 -0
  392. package/src/tools/browser/cdp-client/types.ts +65 -0
  393. package/src/tools/browser/pinned-tabs.ts +96 -40
  394. package/src/tools/computer-use/definitions.ts +22 -78
  395. package/src/tools/credential-execution/make-authenticated-request.ts +3 -9
  396. package/src/tools/credential-execution/manage-secure-command-tool.ts +3 -9
  397. package/src/tools/credential-execution/run-authenticated-command.ts +3 -9
  398. package/src/tools/credentials/vault.ts +3 -9
  399. package/src/tools/document/document-tool.ts +59 -0
  400. package/src/tools/execution-target.ts +21 -23
  401. package/src/tools/executor.ts +6 -1
  402. package/src/tools/filesystem/edit.ts +3 -9
  403. package/src/tools/filesystem/list.ts +3 -9
  404. package/src/tools/filesystem/read.ts +3 -9
  405. package/src/tools/filesystem/write.ts +3 -9
  406. package/src/tools/host-filesystem/edit.ts +3 -9
  407. package/src/tools/host-filesystem/read.ts +3 -9
  408. package/src/tools/host-filesystem/transfer.ts +3 -9
  409. package/src/tools/host-filesystem/write.ts +3 -9
  410. package/src/tools/host-terminal/host-shell.ts +3 -9
  411. package/src/tools/mcp/mcp-tool-factory.ts +1 -8
  412. package/src/tools/memory/register.test.ts +1 -1
  413. package/src/tools/memory/register.ts +4 -9
  414. package/src/tools/network/web-fetch.ts +3 -9
  415. package/src/tools/network/web-search.ts +25 -32
  416. package/src/tools/registry.ts +7 -23
  417. package/src/tools/schema-transforms.ts +1 -1
  418. package/src/tools/skills/execute.ts +3 -9
  419. package/src/tools/skills/load.ts +3 -9
  420. package/src/tools/skills/skill-tool-factory.ts +1 -8
  421. package/src/tools/subagent/notify-parent.ts +3 -9
  422. package/src/tools/system/request-permission.ts +3 -9
  423. package/src/tools/terminal/shell.ts +3 -9
  424. package/src/tools/tool-defaults.ts +94 -0
  425. package/src/tools/types.ts +27 -98
  426. package/src/tools/ui-surface/definitions.ts +6 -22
  427. package/src/usage/pricing.ts +23 -0
  428. package/src/usage/types.ts +12 -0
  429. package/src/util/logger.ts +16 -7
  430. package/src/util/platform.ts +7 -2
  431. package/src/util/sqlite3-runtime.ts +65 -0
  432. package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +1 -0
  433. package/src/workspace/migrations/089-move-memory-tree-out-of-v3.ts +86 -0
  434. package/src/workspace/migrations/registry.ts +2 -0
  435. package/src/__tests__/compaction-strip-metadata-clear.test.ts +0 -206
  436. package/src/__tests__/message-complete-display-id.test.ts +0 -175
  437. package/src/daemon/query-complexity-router.ts +0 -75
  438. package/src/prompts/cache-boundary.ts +0 -8
@@ -0,0 +1,707 @@
1
+ /**
2
+ * Tests for `assistant/src/memory/v3/tree-walk.ts`.
3
+ *
4
+ * The descent provider is always a scripted stub injected via the `provider`
5
+ * arg — no real LLM, no network, no `mock.module`, `~/.vellum/` untouched. The
6
+ * stub keys its scripted decision off the `<node id="...">` marker in the user
7
+ * message so one fixture provider can drive a whole multi-node walk.
8
+ *
9
+ * Coverage:
10
+ * - scripted descent collects the kept leaf pages and records
11
+ * considered/descended/skipped + reasoning per node.
12
+ * - one descent call per *visited node with children* (node or page) — leaf
13
+ * buckets are now judged for page selection, not bulk-collected.
14
+ * - the descender keeps only the pages the model selects (drops the rest).
15
+ * - breadthBudget caps descents per node; maxDepth halts the walk.
16
+ * - the walk starts at root only — scout hits steer it as prompt pressure,
17
+ * not as mid-tree seeds.
18
+ * - provider === null → fail-safe: descend + keep nothing, walk still
19
+ * terminates, reasoning records the failure.
20
+ * - the forced tool exposes keep_pages (enum = offered page slugs).
21
+ * - request shape: forced tool_choice on `choose_branches`, abort signal
22
+ * forwarded.
23
+ */
24
+
25
+ import { describe, expect, test } from "bun:test";
26
+
27
+ import type {
28
+ Message,
29
+ Provider,
30
+ ProviderResponse,
31
+ SendMessageOptions,
32
+ ToolDefinition,
33
+ } from "../../../providers/types.js";
34
+ import type { RetrievalInput } from "../../v2/harness/retriever.js";
35
+ import type { ScoutResult } from "../../v2/harness/trace.js";
36
+ import type { PageIndex } from "../../v2/page-index.js";
37
+ import type { LlmCallRecord } from "../llm-capture.js";
38
+ import { DESCENT_SYSTEM_PROMPT } from "../prompts/system-prompts.js";
39
+ import type { ChildRef, TreeIndex } from "../tree-index.js";
40
+ import { createDescender, runTreeWalk } from "../tree-walk.js";
41
+ import type { TreeNode } from "../types.js";
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Fixture helpers.
45
+ // ---------------------------------------------------------------------------
46
+
47
+ function page(ref: string): ChildRef {
48
+ return { kind: "page", ref };
49
+ }
50
+
51
+ function node(ref: string): ChildRef {
52
+ return { kind: "node", ref };
53
+ }
54
+
55
+ interface ProviderCall {
56
+ messages: Message[];
57
+ tools: ToolDefinition[] | undefined;
58
+ systemPrompt: string | undefined;
59
+ options: SendMessageOptions | undefined;
60
+ }
61
+
62
+ /**
63
+ * Build a tree node with the given children refs. `summary` defaults to the id
64
+ * so `composeNodeIndex` produces deterministic, inspectable lines.
65
+ */
66
+ function makeNode(id: string, children: ChildRef[]): TreeNode {
67
+ return {
68
+ id,
69
+ frontmatter: {
70
+ children: children.map((c) => `${c.kind}:${c.ref}`),
71
+ summary: `summary of ${id}`,
72
+ },
73
+ body: "",
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Build an in-memory `TreeIndex` from a forward-adjacency spec, materializing
79
+ * `nodes`, `childrenByNode`, and the `pageParents` reverse edges. `parentsByNode`
80
+ * is left empty — the driver never reads it.
81
+ */
82
+ function makeTree(
83
+ root: string,
84
+ childrenByNode: Record<string, ChildRef[]>,
85
+ ): TreeIndex {
86
+ const nodes = new Map<string, TreeNode>();
87
+ const children = new Map<string, ReadonlyArray<ChildRef>>();
88
+ const pageParents = new Map<string, Set<string>>();
89
+ for (const [id, refs] of Object.entries(childrenByNode)) {
90
+ nodes.set(id, makeNode(id, refs));
91
+ children.set(id, refs);
92
+ for (const ref of refs) {
93
+ if (ref.kind !== "page") continue;
94
+ let parents = pageParents.get(ref.ref);
95
+ if (!parents) {
96
+ parents = new Set();
97
+ pageParents.set(ref.ref, parents);
98
+ }
99
+ parents.add(id);
100
+ }
101
+ }
102
+ return {
103
+ nodes,
104
+ childrenByNode: children,
105
+ parentsByNode: new Map(),
106
+ pageParents,
107
+ root,
108
+ };
109
+ }
110
+
111
+ /** Empty page index — the driver only needs `bySlug` for page summaries. */
112
+ function makePages(slugs: string[]): PageIndex {
113
+ const bySlug = new Map();
114
+ const byId = new Map();
115
+ let id = 1;
116
+ for (const slug of slugs) {
117
+ const entry = {
118
+ id,
119
+ slug,
120
+ summary: `summary of ${slug}`,
121
+ edges: [],
122
+ modifiedAt: 0,
123
+ };
124
+ bySlug.set(slug, entry);
125
+ byId.set(id, entry);
126
+ id++;
127
+ }
128
+ return { entries: [...bySlug.values()], bySlug, byId, rendered: "" };
129
+ }
130
+
131
+ /** Minimal `RetrievalInput` carrying just the fields the driver reads. */
132
+ function makeInput(
133
+ overrides?: Partial<RetrievalInput> & {
134
+ breadthBudget?: number;
135
+ maxDepth?: number;
136
+ /** Inline override for `memory.v3.prompts.descent`. */
137
+ descentOverride?: string;
138
+ },
139
+ ): RetrievalInput {
140
+ const breadthBudget = overrides?.breadthBudget ?? 8;
141
+ const maxDepth = overrides?.maxDepth ?? 8;
142
+ const config = {
143
+ memory: {
144
+ v3: {
145
+ breadthBudget,
146
+ maxDepth,
147
+ ...(overrides?.descentOverride !== undefined
148
+ ? {
149
+ prompts: {
150
+ descent: { override: overrides.descentOverride, path: null },
151
+ },
152
+ }
153
+ : {}),
154
+ },
155
+ },
156
+ } as unknown as RetrievalInput["config"];
157
+ const {
158
+ breadthBudget: _b,
159
+ maxDepth: _m,
160
+ descentOverride: _d,
161
+ ...rest
162
+ } = overrides ?? {};
163
+ return {
164
+ workspaceDir: "/tmp/does-not-matter",
165
+ recentTurnPairs: [{ assistantMessage: "", userMessage: "tell me about a" }],
166
+ nowText: "2026-05-25 10:00 PT",
167
+ priorEverInjected: [],
168
+ config,
169
+ ...rest,
170
+ };
171
+ }
172
+
173
+ /** Pull the `<node id="...">` id out of a recorded descend prompt. */
174
+ function nodeIdFromCall(call: ProviderCall): string | null {
175
+ for (const block of call.messages[0]?.content ?? []) {
176
+ if (block.type !== "text") continue;
177
+ const match = block.text.match(/<node id="([^"]*)">/);
178
+ if (match) return match[1];
179
+ }
180
+ return null;
181
+ }
182
+
183
+ /** Read the `keep_pages` enum (offered page slugs) out of a built descend tool. */
184
+ function keepPagesEnum(tool: ToolDefinition | undefined): string[] {
185
+ const schema = tool?.input_schema as
186
+ | { properties?: { keep_pages?: { items?: { enum?: string[] } } } }
187
+ | undefined;
188
+ return schema?.properties?.keep_pages?.items?.enum ?? [];
189
+ }
190
+
191
+ /**
192
+ * A scripted descent provider. `script` maps a node id to the bare child-node
193
+ * ids to descend, an optional explicit `keep` (page slugs), and an optional
194
+ * reasoning. When `keep` is omitted the stub keeps *every* offered page (the old
195
+ * bulk behavior), so a test only sets `keep` when exercising selective keeping.
196
+ * Records every call and honors an already-aborted signal by throwing.
197
+ */
198
+ function makeProvider(
199
+ script: Record<
200
+ string,
201
+ { descend: string[]; keep?: string[]; reasoning?: string }
202
+ >,
203
+ calls: ProviderCall[],
204
+ ): Provider {
205
+ return {
206
+ name: "stub",
207
+ sendMessage: async (messages, tools, systemPrompt, options) => {
208
+ calls.push({ messages, tools, systemPrompt, options });
209
+ if (options?.signal?.aborted) {
210
+ const err = new Error("aborted");
211
+ err.name = "AbortError";
212
+ throw err;
213
+ }
214
+ const nodeId =
215
+ nodeIdFromCall({ messages, tools, systemPrompt, options }) ?? "";
216
+ const decision = script[nodeId] ?? { descend: [] };
217
+ const keep = decision.keep ?? keepPagesEnum(tools?.[0]);
218
+ const input: Record<string, unknown> = {
219
+ descend: decision.descend,
220
+ keep_pages: keep,
221
+ };
222
+ if (decision.reasoning !== undefined)
223
+ input.reasoning = decision.reasoning;
224
+ const response: ProviderResponse = {
225
+ model: "stub-model",
226
+ stopReason: "tool_use",
227
+ usage: { inputTokens: 0, outputTokens: 0 },
228
+ content: [
229
+ {
230
+ type: "tool_use",
231
+ id: `tu-${nodeId}`,
232
+ name: "choose_branches",
233
+ input,
234
+ },
235
+ ],
236
+ };
237
+ return response;
238
+ },
239
+ };
240
+ }
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // runTreeWalk — scripted descent
244
+ // ---------------------------------------------------------------------------
245
+
246
+ describe("runTreeWalk — scripted descent", () => {
247
+ test("collects the kept leaf pages and records the descend/skip split", async () => {
248
+ // _root → {a, b}; a → leaf pa; b → leaf pb. Script descends only "a".
249
+ const tree = makeTree("_root", {
250
+ _root: [node("a"), node("b")],
251
+ a: [page("pa")],
252
+ b: [page("pb")],
253
+ });
254
+ const pages = makePages(["pa", "pb"]);
255
+ const calls: ProviderCall[] = [];
256
+ const provider = makeProvider(
257
+ { _root: { descend: ["a"], reasoning: "a matches the turn" } },
258
+ calls,
259
+ );
260
+
261
+ const { pages: collected, levels } = await runTreeWalk({
262
+ input: makeInput(),
263
+ tree,
264
+ pages,
265
+ scouts: [],
266
+ provider,
267
+ });
268
+
269
+ // Only the descended branch's page is kept; b is skipped entirely.
270
+ expect([...collected]).toEqual(["pa"]);
271
+
272
+ const rootLevel = levels.find((l) => l.node === "_root")!;
273
+ expect(rootLevel.considered).toEqual(["a", "b"]);
274
+ expect(rootLevel.descended).toEqual(["a"]);
275
+ expect(rootLevel.skipped).toEqual(["b"]);
276
+ expect(rootLevel.reasoning).toBe("a matches the turn");
277
+
278
+ // _root (node children) and a (page child) are both walked; b is skipped.
279
+ expect(levels.map((l) => l.node).sort()).toEqual(["_root", "a"]);
280
+ });
281
+
282
+ test("keeps only the pages the model selects at a node", async () => {
283
+ // _root → a, a leaf bucket of two pages. The model keeps only one.
284
+ const tree = makeTree("_root", {
285
+ _root: [node("a")],
286
+ a: [page("pa-keep"), page("pa-drop")],
287
+ });
288
+ const pages = makePages(["pa-keep", "pa-drop"]);
289
+ const calls: ProviderCall[] = [];
290
+ const provider = makeProvider(
291
+ {
292
+ _root: { descend: ["a"] },
293
+ a: { descend: [], keep: ["pa-keep"] },
294
+ },
295
+ calls,
296
+ );
297
+
298
+ const { pages: collected } = await runTreeWalk({
299
+ input: makeInput(),
300
+ tree,
301
+ pages,
302
+ scouts: [],
303
+ provider,
304
+ });
305
+
306
+ expect([...collected]).toEqual(["pa-keep"]);
307
+ });
308
+
309
+ test("makes one descent call per visited node with children", async () => {
310
+ const tree = makeTree("_root", {
311
+ _root: [node("a"), node("b")],
312
+ a: [node("c"), page("pa")],
313
+ b: [page("pb")],
314
+ c: [page("pc")],
315
+ });
316
+ const pages = makePages(["pa", "pb", "pc"]);
317
+ const calls: ProviderCall[] = [];
318
+ const provider = makeProvider(
319
+ { _root: { descend: ["a", "b"] }, a: { descend: ["c"] } },
320
+ calls,
321
+ );
322
+
323
+ await runTreeWalk({
324
+ input: makeInput(),
325
+ tree,
326
+ pages,
327
+ scouts: [],
328
+ provider,
329
+ });
330
+
331
+ // Every visited node has children (b and c are page-only leaf buckets that
332
+ // now get a page-selection call too), so all four are called.
333
+ const calledNodes = calls.map(nodeIdFromCall).sort();
334
+ expect(calledNodes).toEqual(["_root", "a", "b", "c"]);
335
+ });
336
+
337
+ test("breadthBudget caps descents per node", async () => {
338
+ const tree = makeTree("_root", {
339
+ _root: [node("a"), node("b"), node("c")],
340
+ a: [page("pa")],
341
+ b: [page("pb")],
342
+ c: [page("pc")],
343
+ });
344
+ const pages = makePages(["pa", "pb", "pc"]);
345
+ const calls: ProviderCall[] = [];
346
+ // Model picks all three; budget 2 admits only the first two.
347
+ const provider = makeProvider(
348
+ { _root: { descend: ["a", "b", "c"] } },
349
+ calls,
350
+ );
351
+
352
+ const { pages: collected, levels } = await runTreeWalk({
353
+ input: makeInput({ breadthBudget: 2 }),
354
+ tree,
355
+ pages,
356
+ scouts: [],
357
+ provider,
358
+ });
359
+
360
+ const rootLevel = levels.find((l) => l.node === "_root")!;
361
+ expect(rootLevel.descended).toEqual(["a", "b"]);
362
+ expect(rootLevel.skipped).toEqual(["c"]);
363
+ expect([...collected].sort()).toEqual(["pa", "pb"]);
364
+ });
365
+
366
+ test("maxDepth halts the walk", async () => {
367
+ const tree = makeTree("_root", {
368
+ _root: [node("a")],
369
+ a: [node("b"), page("pa")],
370
+ b: [page("pb")],
371
+ });
372
+ const pages = makePages(["pa", "pb"]);
373
+ const calls: ProviderCall[] = [];
374
+ const provider = makeProvider(
375
+ { _root: { descend: ["a"] }, a: { descend: ["b"] } },
376
+ calls,
377
+ );
378
+
379
+ const { pages: collected, levels } = await runTreeWalk({
380
+ input: makeInput({ maxDepth: 1 }),
381
+ tree,
382
+ pages,
383
+ scouts: [],
384
+ provider,
385
+ });
386
+
387
+ // Depth 0 (_root) and depth 1 (a) walked; b never reached.
388
+ expect(levels.map((l) => l.node)).toEqual(["_root", "a"]);
389
+ expect([...collected]).toEqual(["pa"]);
390
+ });
391
+ });
392
+
393
+ // ---------------------------------------------------------------------------
394
+ // runTreeWalk — root-only walk + scout pressure
395
+ // ---------------------------------------------------------------------------
396
+
397
+ describe("runTreeWalk — root-only walk", () => {
398
+ test("starts at root only — a scout hit in an unreachable subtree is not walked", async () => {
399
+ // root only links to `a`; `island` is unreachable from root. A scout
400
+ // surfaced its leaf page, but the walk no longer seeds at scout parents.
401
+ const tree = makeTree("_root", {
402
+ _root: [node("a")],
403
+ a: [page("pa")],
404
+ island: [page("treasure")],
405
+ });
406
+ const pages = makePages(["pa", "treasure"]);
407
+ const calls: ProviderCall[] = [];
408
+ const provider = makeProvider({ _root: { descend: ["a"] } }, calls);
409
+
410
+ const scouts: ScoutResult[] = [{ lane: "dense", slugs: ["treasure"] }];
411
+ const { pages: collected, levels } = await runTreeWalk({
412
+ input: makeInput(),
413
+ tree,
414
+ pages,
415
+ scouts,
416
+ provider,
417
+ });
418
+
419
+ // Only the root branch is walked; `island`/treasure is never reached.
420
+ expect([...collected]).toEqual(["pa"]);
421
+ expect(levels.map((l) => l.node).sort()).toEqual(["_root", "a"]);
422
+ });
423
+
424
+ test("renders scout hits into the descend prompt as pressure", async () => {
425
+ const tree = makeTree("_root", {
426
+ _root: [node("a"), node("b")],
427
+ a: [page("pa")],
428
+ b: [page("pb")],
429
+ });
430
+ const pages = makePages(["pa", "pb"]);
431
+ const calls: ProviderCall[] = [];
432
+ const provider = makeProvider({ _root: { descend: ["a"] } }, calls);
433
+
434
+ const scouts: ScoutResult[] = [{ lane: "sparse", slugs: ["pb"] }];
435
+ await runTreeWalk({
436
+ input: makeInput(),
437
+ tree,
438
+ pages,
439
+ scouts,
440
+ provider,
441
+ });
442
+
443
+ const rootCall = calls.find((c) => nodeIdFromCall(c) === "_root")!;
444
+ const promptText = rootCall.messages[0].content
445
+ .filter((b) => b.type === "text")
446
+ .map((b) => (b as { text: string }).text)
447
+ .join("\n");
448
+ expect(promptText).toContain("<scout_hits>");
449
+ expect(promptText).toContain("[sparse]: pb");
450
+ });
451
+ });
452
+
453
+ // ---------------------------------------------------------------------------
454
+ // runTreeWalk — fail-safe
455
+ // ---------------------------------------------------------------------------
456
+
457
+ describe("runTreeWalk — fail-safe", () => {
458
+ test("provider null descends and keeps nothing but still terminates", async () => {
459
+ const tree = makeTree("_root", {
460
+ _root: [node("a"), page("pr")],
461
+ a: [page("pa")],
462
+ });
463
+ const pages = makePages(["pr", "pa"]);
464
+
465
+ const { pages: collected, levels } = await runTreeWalk({
466
+ input: makeInput(),
467
+ tree,
468
+ pages,
469
+ scouts: [],
470
+ provider: null,
471
+ });
472
+
473
+ // No provider → the node keeps nothing (the scout lanes carry recall in the
474
+ // loop), descends nothing, and the walk stops at root.
475
+ expect([...collected]).toEqual([]);
476
+ expect(levels.map((l) => l.node)).toEqual(["_root"]);
477
+ const rootLevel = levels[0];
478
+ expect(rootLevel.descended).toEqual([]);
479
+ expect(rootLevel.skipped).toEqual(["a"]);
480
+ expect(rootLevel.reasoning).toContain("no provider");
481
+ });
482
+
483
+ test("malformed tool input fails closed for that node", async () => {
484
+ const tree = makeTree("_root", {
485
+ _root: [node("a")],
486
+ a: [page("pa")],
487
+ });
488
+ const pages = makePages(["pa"]);
489
+ const calls: ProviderCall[] = [];
490
+ // Provider returns a non-conforming tool input (descend is not an array).
491
+ const provider: Provider = {
492
+ name: "bad-schema",
493
+ sendMessage: async (messages, tools, systemPrompt, options) => {
494
+ calls.push({ messages, tools, systemPrompt, options });
495
+ return {
496
+ model: "stub-model",
497
+ stopReason: "tool_use",
498
+ usage: { inputTokens: 0, outputTokens: 0 },
499
+ content: [
500
+ {
501
+ type: "tool_use",
502
+ id: "tu-1",
503
+ name: "choose_branches",
504
+ input: { descend: "not-an-array" },
505
+ },
506
+ ],
507
+ };
508
+ },
509
+ };
510
+
511
+ const { levels } = await runTreeWalk({
512
+ input: makeInput(),
513
+ tree,
514
+ pages,
515
+ scouts: [],
516
+ provider,
517
+ });
518
+
519
+ const rootLevel = levels.find((l) => l.node === "_root")!;
520
+ expect(rootLevel.descended).toEqual([]);
521
+ expect(rootLevel.reasoning).toContain("validation");
522
+ });
523
+ });
524
+
525
+ // ---------------------------------------------------------------------------
526
+ // createDescender — request shape + page selection
527
+ // ---------------------------------------------------------------------------
528
+
529
+ describe("createDescender — request shape", () => {
530
+ test("forces tool_choice on choose_branches and forwards the abort signal", async () => {
531
+ const tree = makeTree("_root", {
532
+ _root: [node("a")],
533
+ a: [page("pa")],
534
+ });
535
+ const pages = makePages(["pa"]);
536
+ const calls: ProviderCall[] = [];
537
+ const provider = makeProvider({ _root: { descend: ["a"] } }, calls);
538
+
539
+ const descender = createDescender({
540
+ input: makeInput({ signal: AbortSignal.timeout(10_000) }),
541
+ tree,
542
+ pages,
543
+ scouts: [],
544
+ provider,
545
+ });
546
+
547
+ await descender("_root", [...tree.childrenByNode.get("_root")!]);
548
+
549
+ expect(calls).toHaveLength(1);
550
+ const call = calls[0];
551
+ expect(call.tools?.[0]?.name).toBe("choose_branches");
552
+ expect(call.options?.config?.tool_choice).toEqual({
553
+ type: "tool",
554
+ name: "choose_branches",
555
+ });
556
+ expect(call.options?.config?.callSite).toBe("memoryV3Descent");
557
+ expect(call.options?.signal).toBeDefined();
558
+ });
559
+
560
+ test("exposes a keep_pages enum of the node's offered page slugs", async () => {
561
+ const tree = makeTree("bucket", {
562
+ bucket: [page("p1"), page("p2")],
563
+ });
564
+ const pages = makePages(["p1", "p2"]);
565
+ const calls: ProviderCall[] = [];
566
+ const provider = makeProvider({}, calls);
567
+
568
+ const descender = createDescender({
569
+ input: makeInput(),
570
+ tree,
571
+ pages,
572
+ scouts: [],
573
+ provider,
574
+ });
575
+
576
+ await descender("bucket", [...tree.childrenByNode.get("bucket")!]);
577
+
578
+ expect(keepPagesEnum(calls[0].tools?.[0]).sort()).toEqual(["p1", "p2"]);
579
+ });
580
+
581
+ test("a node with no children at all makes no provider call", async () => {
582
+ const tree = makeTree("empty", { empty: [] });
583
+ const pages = makePages([]);
584
+ const calls: ProviderCall[] = [];
585
+ const provider = makeProvider({}, calls);
586
+
587
+ const descender = createDescender({
588
+ input: makeInput(),
589
+ tree,
590
+ pages,
591
+ scouts: [],
592
+ provider,
593
+ });
594
+
595
+ const result = await descender("empty", [
596
+ ...(tree.childrenByNode.get("empty") ?? []),
597
+ ]);
598
+ expect(result).toEqual({ descend: [], keep: [], reasoning: "" });
599
+ expect(calls).toHaveLength(0);
600
+ });
601
+
602
+ test("a leaf bucket of pages makes a call and keeps the selected pages", async () => {
603
+ const tree = makeTree("leaf", { leaf: [page("p1"), page("p2")] });
604
+ const pages = makePages(["p1", "p2"]);
605
+ const calls: ProviderCall[] = [];
606
+ // Keep only p1.
607
+ const provider = makeProvider(
608
+ { leaf: { descend: [], keep: ["p1"] } },
609
+ calls,
610
+ );
611
+
612
+ const descender = createDescender({
613
+ input: makeInput(),
614
+ tree,
615
+ pages,
616
+ scouts: [],
617
+ provider,
618
+ });
619
+
620
+ const result = await descender("leaf", [
621
+ ...tree.childrenByNode.get("leaf")!,
622
+ ]);
623
+
624
+ expect(calls).toHaveLength(1);
625
+ expect(result.descend).toEqual([]);
626
+ expect(result.keep).toEqual([page("p1")]);
627
+ });
628
+ });
629
+
630
+ // ---------------------------------------------------------------------------
631
+ // runTreeWalk — descent system prompt
632
+ // ---------------------------------------------------------------------------
633
+
634
+ describe("runTreeWalk — descent system prompt", () => {
635
+ const tree = makeTree("_root", {
636
+ _root: [node("a")],
637
+ a: [page("pa")],
638
+ });
639
+
640
+ test("uses the bundled default when no override is configured", async () => {
641
+ const pages = makePages(["pa"]);
642
+ const calls: ProviderCall[] = [];
643
+ const provider = makeProvider({ _root: { descend: ["a"] } }, calls);
644
+
645
+ await runTreeWalk({
646
+ input: makeInput(),
647
+ tree,
648
+ pages,
649
+ scouts: [],
650
+ provider,
651
+ });
652
+
653
+ const rootCall = calls.find((c) => nodeIdFromCall(c) === "_root")!;
654
+ expect(rootCall.systemPrompt).toBe(DESCENT_SYSTEM_PROMPT);
655
+ });
656
+
657
+ test("uses the configured inline override as the descent system prompt", async () => {
658
+ const pages = makePages(["pa"]);
659
+ const calls: ProviderCall[] = [];
660
+ const provider = makeProvider({ _root: { descend: ["a"] } }, calls);
661
+
662
+ const override = "CUSTOM DESCENT PROMPT — descend everything plausible.";
663
+ await runTreeWalk({
664
+ input: makeInput({ descentOverride: override }),
665
+ tree,
666
+ pages,
667
+ scouts: [],
668
+ provider,
669
+ });
670
+
671
+ const rootCall = calls.find((c) => nodeIdFromCall(c) === "_root")!;
672
+ expect(rootCall.systemPrompt).toBe(override);
673
+ expect(rootCall.systemPrompt).not.toBe(DESCENT_SYSTEM_PROMPT);
674
+ });
675
+ });
676
+
677
+ describe("runTreeWalk — capture", () => {
678
+ test("emits one record per descender LLM call, tagged with the node", async () => {
679
+ // _root has node children but descends nothing, so exactly one call fires.
680
+ const tree = makeTree("_root", {
681
+ _root: [node("a"), node("b")],
682
+ a: [page("pa")],
683
+ b: [page("pb")],
684
+ });
685
+ const pages = makePages(["pa", "pb"]);
686
+ const calls: ProviderCall[] = [];
687
+ const provider = makeProvider({ _root: { descend: [] } }, calls);
688
+ const captured: Omit<LlmCallRecord, "pass">[] = [];
689
+
690
+ await runTreeWalk({
691
+ input: makeInput(),
692
+ tree,
693
+ pages,
694
+ scouts: [],
695
+ provider,
696
+ capture: (record) => captured.push(record),
697
+ });
698
+
699
+ expect(captured).toHaveLength(1);
700
+ const rec = captured[0]!;
701
+ expect(rec.lane).toBe("descent");
702
+ expect(rec.callSite).toBe("memoryV3Descent");
703
+ expect(rec.node).toBe("_root");
704
+ expect(rec.request.tools[0]!.name).toBe("choose_branches");
705
+ expect(rec.response.stopReason).toBe("tool_use");
706
+ });
707
+ });