@vellumai/assistant 0.6.5 → 0.6.6

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 (443) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +15 -17
  3. package/Dockerfile +6 -4
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  5. package/docs/architecture/integrations.md +32 -39
  6. package/docs/architecture/memory.md +25 -30
  7. package/docs/architecture/security.md +7 -6
  8. package/docs/browser-use-architecture-phase2.md +63 -20
  9. package/docs/plugins.md +761 -0
  10. package/examples/plugins/echo/README.md +132 -0
  11. package/examples/plugins/echo/package.json +17 -0
  12. package/examples/plugins/echo/register.ts +187 -0
  13. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  14. package/openapi.yaml +212 -68
  15. package/package.json +1 -1
  16. package/src/__tests__/app-compiler.test.ts +57 -0
  17. package/src/__tests__/approval-cascade.test.ts +7 -2
  18. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  19. package/src/__tests__/avatar-generator.test.ts +4 -2
  20. package/src/__tests__/bundled-asset.test.ts +6 -6
  21. package/src/__tests__/catalog-cache.test.ts +69 -0
  22. package/src/__tests__/checker.test.ts +459 -171
  23. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  24. package/src/__tests__/compaction-events.test.ts +501 -0
  25. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  26. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  27. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  28. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  29. package/src/__tests__/config-schema.test.ts +22 -9
  30. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  31. package/src/__tests__/contacts-tools.test.ts +26 -0
  32. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  33. package/src/__tests__/context-window-manager.test.ts +355 -4
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  35. package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
  36. package/src/__tests__/conversation-agent-loop.test.ts +30 -141
  37. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
  38. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  39. package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
  40. package/src/__tests__/conversation-pairing.test.ts +174 -10
  41. package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
  42. package/src/__tests__/conversation-process-callsite.test.ts +3 -0
  43. package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
  44. package/src/__tests__/conversation-queue.test.ts +29 -14
  45. package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
  48. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  49. package/src/__tests__/conversation-slash-queue.test.ts +7 -2
  50. package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
  51. package/src/__tests__/conversation-speed-override.test.ts +6 -1
  52. package/src/__tests__/conversation-title-service.test.ts +116 -0
  53. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  54. package/src/__tests__/conversation-usage.test.ts +1 -1
  55. package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
  56. package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
  57. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
  58. package/src/__tests__/credential-health-service.test.ts +78 -9
  59. package/src/__tests__/credential-security-invariants.test.ts +2 -2
  60. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  61. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  62. package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
  63. package/src/__tests__/first-greeting.test.ts +247 -5
  64. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  65. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  66. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  67. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  68. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  69. package/src/__tests__/image-credentials.test.ts +137 -0
  70. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  71. package/src/__tests__/injector-chain.test.ts +526 -0
  72. package/src/__tests__/intent-routing.test.ts +0 -26
  73. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  74. package/src/__tests__/llm-schema.test.ts +1 -1
  75. package/src/__tests__/media-generate-image.test.ts +119 -13
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  77. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  78. package/src/__tests__/migration-import-from-url.test.ts +5 -68
  79. package/src/__tests__/model-intents.test.ts +4 -2
  80. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  81. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  82. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  83. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  84. package/src/__tests__/oauth-cli.test.ts +14 -12
  85. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  86. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  87. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  88. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  89. package/src/__tests__/oauth-store.test.ts +41 -76
  90. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  91. package/src/__tests__/openai-image-service.test.ts +368 -0
  92. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  93. package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
  94. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  95. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  96. package/src/__tests__/pipeline-runner.test.ts +565 -0
  97. package/src/__tests__/platform.test.ts +5 -2
  98. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  99. package/src/__tests__/plugin-registry.test.ts +273 -0
  100. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  101. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  102. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  103. package/src/__tests__/plugin-types.test.ts +320 -0
  104. package/src/__tests__/pricing.test.ts +44 -12
  105. package/src/__tests__/proxy-approval-callback.test.ts +69 -8
  106. package/src/__tests__/reaction-persistence.test.ts +1 -0
  107. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  108. package/src/__tests__/registry.test.ts +0 -2
  109. package/src/__tests__/schedule-routes.test.ts +131 -1
  110. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  111. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  112. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  113. package/src/__tests__/shell-identity.test.ts +0 -134
  114. package/src/__tests__/suggestion-routes.test.ts +103 -4
  115. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  116. package/src/__tests__/task-scheduler.test.ts +3 -15
  117. package/src/__tests__/test-preload.ts +11 -0
  118. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  119. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  120. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  121. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  122. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
  123. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  124. package/src/__tests__/tool-executor.test.ts +141 -0
  125. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  126. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  127. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  128. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  129. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  130. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  131. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  132. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  133. package/src/__tests__/workspace-policy.test.ts +21 -3
  134. package/src/agent/loop.ts +340 -102
  135. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  136. package/src/approvals/guardian-request-resolvers.ts +80 -0
  137. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  138. package/src/backup/backup-worker.ts +3 -15
  139. package/src/bundler/app-compiler.ts +84 -1
  140. package/src/calls/call-state.ts +2 -2
  141. package/src/channels/__tests__/types.test.ts +3 -3
  142. package/src/channels/types.ts +6 -4
  143. package/src/cli/__tests__/notifications.test.ts +87 -211
  144. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  145. package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
  146. package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
  147. package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
  148. package/src/cli/commands/backup.ts +2 -2
  149. package/src/cli/commands/clients.ts +138 -0
  150. package/src/cli/commands/completions.ts +2 -9
  151. package/src/cli/commands/conversations.ts +55 -7
  152. package/src/cli/commands/image-generation.ts +33 -34
  153. package/src/cli/commands/notifications.ts +68 -103
  154. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  155. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  156. package/src/cli/commands/oauth/connect.ts +2 -2
  157. package/src/cli/commands/oauth/providers.ts +176 -8
  158. package/src/cli/commands/oauth/status.ts +46 -36
  159. package/src/cli/commands/skills.ts +3 -4
  160. package/src/cli/program.ts +25 -29
  161. package/src/config/__tests__/backup-schema.test.ts +7 -2
  162. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  163. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  164. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  165. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  166. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  170. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  171. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  172. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
  174. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  175. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  176. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  177. package/src/config/bundled-tool-registry.ts +0 -15
  178. package/src/config/feature-flag-registry.json +17 -1
  179. package/src/config/schema.ts +19 -0
  180. package/src/config/schemas/backup.ts +1 -1
  181. package/src/config/schemas/conversations.ts +16 -0
  182. package/src/config/schemas/llm.ts +2 -3
  183. package/src/config/schemas/security.ts +6 -6
  184. package/src/config/schemas/tts.ts +11 -0
  185. package/src/config/skill-state.ts +6 -2
  186. package/src/config/skills.ts +94 -5
  187. package/src/context/__tests__/compact-prompt.test.ts +27 -9
  188. package/src/context/prompts/compact.md +26 -12
  189. package/src/context/tool-result-truncation.ts +3 -63
  190. package/src/context/window-manager.ts +190 -16
  191. package/src/credential-health/credential-health-service.ts +19 -6
  192. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  193. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  194. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  195. package/src/daemon/config-watcher.ts +0 -2
  196. package/src/daemon/context-overflow-policy.ts +4 -13
  197. package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
  198. package/src/daemon/conversation-agent-loop.ts +984 -683
  199. package/src/daemon/conversation-history.ts +10 -19
  200. package/src/daemon/conversation-lifecycle.ts +37 -19
  201. package/src/daemon/conversation-notifiers.ts +2 -110
  202. package/src/daemon/conversation-process.ts +14 -7
  203. package/src/daemon/conversation-runtime-assembly.ts +532 -411
  204. package/src/daemon/conversation-tool-setup.ts +41 -4
  205. package/src/daemon/conversation.ts +80 -35
  206. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  207. package/src/daemon/first-greeting.ts +191 -14
  208. package/src/daemon/handlers/config-model.ts +11 -0
  209. package/src/daemon/handlers/skills.ts +5 -1
  210. package/src/daemon/lifecycle.ts +33 -68
  211. package/src/daemon/message-types/computer-use.ts +2 -34
  212. package/src/daemon/message-types/conversations.ts +49 -0
  213. package/src/daemon/message-types/messages.ts +12 -0
  214. package/src/daemon/server.ts +5 -3
  215. package/src/daemon/shutdown-handlers.ts +2 -12
  216. package/src/daemon/tool-side-effects.ts +14 -56
  217. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  218. package/src/heartbeat/heartbeat-service.ts +24 -1
  219. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  220. package/src/home/emit-feed-event.ts +7 -0
  221. package/src/home/feed-types.ts +41 -2
  222. package/src/home/rewrite-command-preview.ts +66 -0
  223. package/src/ipc/__tests__/socket-path.test.ts +11 -50
  224. package/src/ipc/cli-client.ts +1 -1
  225. package/src/ipc/cli-server.ts +3 -3
  226. package/src/ipc/gateway-client.ts +4 -1
  227. package/src/ipc/routes/browser-context.ts +2 -0
  228. package/src/ipc/routes/browser.ts +1 -0
  229. package/src/ipc/routes/get-contact.ts +16 -0
  230. package/src/ipc/routes/index.ts +14 -0
  231. package/src/ipc/routes/list-clients.ts +31 -0
  232. package/src/ipc/routes/merge-contacts.ts +17 -0
  233. package/src/ipc/routes/notification.ts +133 -0
  234. package/src/ipc/routes/rename-conversation.ts +59 -0
  235. package/src/ipc/routes/search-contacts.ts +19 -0
  236. package/src/ipc/routes/upsert-contact.ts +25 -0
  237. package/src/ipc/socket-path.ts +14 -38
  238. package/src/media/app-icon-generator.ts +23 -46
  239. package/src/media/avatar-router.ts +26 -41
  240. package/src/media/gemini-image-service.ts +8 -41
  241. package/src/media/image-credentials.ts +73 -0
  242. package/src/media/image-service.ts +85 -0
  243. package/src/media/openai-image-service.ts +131 -0
  244. package/src/media/types.ts +46 -0
  245. package/src/memory/conversation-crud.ts +48 -18
  246. package/src/memory/conversation-queries.ts +57 -4
  247. package/src/memory/conversation-title-service.ts +25 -0
  248. package/src/memory/db-init.ts +8 -0
  249. package/src/memory/embedding-gemini.test.ts +41 -2
  250. package/src/memory/embedding-gemini.ts +6 -1
  251. package/src/memory/graph/bootstrap.test.ts +282 -0
  252. package/src/memory/graph/bootstrap.ts +8 -5
  253. package/src/memory/graph/extraction.ts +10 -2
  254. package/src/memory/graph/graph-search.test.ts +1 -0
  255. package/src/memory/graph/inspect.ts +2 -2
  256. package/src/memory/graph/retriever.ts +10 -3
  257. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  258. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  259. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  260. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  261. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  262. package/src/memory/migrations/index.ts +4 -0
  263. package/src/memory/pkb/pkb-index.test.ts +1 -0
  264. package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
  265. package/src/memory/pkb/pkb-search.test.ts +65 -4
  266. package/src/memory/pkb/pkb-search.ts +40 -18
  267. package/src/memory/qdrant-client.test.ts +60 -0
  268. package/src/memory/qdrant-client.ts +25 -0
  269. package/src/memory/schema/infrastructure.ts +1 -0
  270. package/src/memory/schema/oauth.ts +4 -1
  271. package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
  272. package/src/messaging/providers/slack/render-transcript.ts +58 -0
  273. package/src/notifications/conversation-pairing.ts +78 -19
  274. package/src/notifications/copy-composer.ts +0 -5
  275. package/src/notifications/emit-signal.ts +1 -1
  276. package/src/notifications/signal.ts +1 -2
  277. package/src/oauth/AGENTS.md +1 -1
  278. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  279. package/src/oauth/connect-orchestrator.ts +8 -34
  280. package/src/oauth/connect-types.ts +6 -10
  281. package/src/oauth/manual-token-connection.ts +23 -0
  282. package/src/oauth/oauth-store.ts +30 -14
  283. package/src/oauth/provider-serializer.ts +6 -1
  284. package/src/oauth/seed-providers.ts +56 -108
  285. package/src/outbound-proxy/http-forwarder.ts +9 -0
  286. package/src/permissions/approval-policy.test.ts +293 -18
  287. package/src/permissions/approval-policy.ts +110 -58
  288. package/src/permissions/arg-parser.test.ts +161 -0
  289. package/src/permissions/arg-parser.ts +141 -0
  290. package/src/permissions/bash-risk-classifier.test.ts +414 -2
  291. package/src/permissions/bash-risk-classifier.ts +303 -60
  292. package/src/permissions/checker.ts +157 -29
  293. package/src/permissions/command-registry.test.ts +239 -0
  294. package/src/permissions/command-registry.ts +234 -54
  295. package/src/permissions/defaults.ts +5 -4
  296. package/src/permissions/gateway-threshold-reader.ts +196 -0
  297. package/src/permissions/prompter.ts +4 -0
  298. package/src/permissions/risk-types.ts +61 -4
  299. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  300. package/src/permissions/schedule-risk-classifier.ts +85 -0
  301. package/src/permissions/shell-identity.ts +2 -42
  302. package/src/permissions/types.ts +2 -0
  303. package/src/permissions/workspace-policy.ts +8 -3
  304. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  305. package/src/plugins/defaults/compaction.ts +145 -0
  306. package/src/plugins/defaults/empty-response.ts +126 -0
  307. package/src/plugins/defaults/history-repair.ts +85 -0
  308. package/src/plugins/defaults/index.ts +116 -0
  309. package/src/plugins/defaults/injectors.ts +491 -0
  310. package/src/plugins/defaults/llm-call.ts +82 -0
  311. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  312. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  313. package/src/plugins/defaults/persistence.ts +129 -0
  314. package/src/plugins/defaults/title-generate.ts +95 -0
  315. package/src/plugins/defaults/token-estimate.ts +104 -0
  316. package/src/plugins/defaults/tool-error.ts +126 -0
  317. package/src/plugins/defaults/tool-execute.ts +89 -0
  318. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  319. package/src/plugins/pipeline.ts +316 -0
  320. package/src/plugins/plugin-skill-contributions.ts +292 -0
  321. package/src/plugins/registry.ts +241 -0
  322. package/src/plugins/types.ts +1134 -0
  323. package/src/plugins/user-loader.ts +177 -0
  324. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  325. package/src/providers/model-catalog.ts +52 -29
  326. package/src/providers/model-intents.ts +1 -1
  327. package/src/providers/openrouter/client.ts +5 -1
  328. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  329. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  330. package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
  331. package/src/providers/speech-to-text/xai-realtime.ts +39 -14
  332. package/src/runtime/AGENTS.md +25 -16
  333. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  334. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  335. package/src/runtime/client-registry.ts +261 -0
  336. package/src/runtime/http-server.ts +77 -8
  337. package/src/runtime/http-types.ts +0 -2
  338. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  339. package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
  340. package/src/runtime/routes/approval-routes.ts +17 -0
  341. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  342. package/src/runtime/routes/conversation-routes.ts +223 -116
  343. package/src/runtime/routes/inbound-message-handler.ts +88 -13
  344. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  345. package/src/runtime/routes/migration-routes.ts +0 -3
  346. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  347. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  348. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  349. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  350. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  351. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  352. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  353. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  354. package/src/runtime/routes/playground/deps.ts +56 -0
  355. package/src/runtime/routes/playground/force-compact.ts +73 -0
  356. package/src/runtime/routes/playground/guard.ts +37 -0
  357. package/src/runtime/routes/playground/index.ts +28 -0
  358. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  359. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  360. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  361. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  362. package/src/runtime/routes/playground/state.ts +78 -0
  363. package/src/runtime/routes/schedule-routes.ts +89 -8
  364. package/src/runtime/skill-route-registry.ts +75 -15
  365. package/src/schedule/run-script.ts +68 -0
  366. package/src/schedule/schedule-store.ts +7 -1
  367. package/src/schedule/scheduler.ts +48 -8
  368. package/src/skills/catalog-cache.ts +12 -5
  369. package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
  370. package/src/tools/browser/browser-execution.ts +88 -19
  371. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  372. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  373. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  374. package/src/tools/browser/cdp-client/factory.ts +15 -4
  375. package/src/tools/executor.ts +126 -74
  376. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  377. package/src/tools/permission-checker.ts +98 -49
  378. package/src/tools/policy-context.ts +4 -0
  379. package/src/tools/registry.ts +140 -3
  380. package/src/tools/schedule/create.ts +23 -8
  381. package/src/tools/schedule/update.ts +3 -1
  382. package/src/tools/secret-detection-handler.ts +0 -51
  383. package/src/tools/system/avatar-generator.ts +6 -2
  384. package/src/tools/types.ts +28 -2
  385. package/src/util/platform.ts +7 -2
  386. package/src/util/pricing.ts +26 -3
  387. package/src/workspace/migrations/006-services-config.ts +2 -4
  388. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  389. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
  390. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  391. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  392. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  393. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  394. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  395. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  396. package/src/workspace/migrations/registry.ts +12 -0
  397. package/tsconfig.json +1 -1
  398. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  399. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  400. package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
  401. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  402. package/src/__tests__/hooks-blocking.test.ts +0 -178
  403. package/src/__tests__/hooks-cli.test.ts +0 -182
  404. package/src/__tests__/hooks-config.test.ts +0 -108
  405. package/src/__tests__/hooks-discovery.test.ts +0 -211
  406. package/src/__tests__/hooks-integration.test.ts +0 -196
  407. package/src/__tests__/hooks-manager.test.ts +0 -226
  408. package/src/__tests__/hooks-runner.test.ts +0 -175
  409. package/src/__tests__/hooks-settings.test.ts +0 -160
  410. package/src/__tests__/hooks-templates.test.ts +0 -169
  411. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  412. package/src/__tests__/hooks-watch.test.ts +0 -112
  413. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  414. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  415. package/src/__tests__/send-notification-tool.test.ts +0 -83
  416. package/src/cli/commands/shotgun.ts +0 -266
  417. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  418. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  419. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
  420. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  421. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  422. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  423. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  424. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  425. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  426. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  427. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  428. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  429. package/src/daemon/context-overflow-approval.ts +0 -52
  430. package/src/daemon/watch-handler.ts +0 -399
  431. package/src/hooks/cli.ts +0 -253
  432. package/src/hooks/config.ts +0 -100
  433. package/src/hooks/discovery.ts +0 -135
  434. package/src/hooks/manager.ts +0 -179
  435. package/src/hooks/runner.ts +0 -117
  436. package/src/hooks/templates.ts +0 -77
  437. package/src/hooks/types.ts +0 -75
  438. package/src/oauth/scope-policy.ts +0 -89
  439. package/src/runtime/gateway-internal-client.ts +0 -94
  440. package/src/runtime/routes/watch-routes.ts +0 -156
  441. package/src/signals/shotgun.ts +0 -203
  442. package/src/tools/watch/screen-watch.ts +0 -144
  443. package/src/tools/watch/watch-state.ts +0 -142
@@ -86,6 +86,16 @@ let checkFnOverride:
86
86
  /** Override for generateScopeOptions — when set, returns this value instead of the default. */
87
87
  let scopeOptionsOverride: ScopeOption[] | undefined;
88
88
 
89
+ /** Override for getCachedAssessment — when set, returns this value. */
90
+ let cachedAssessmentOverride:
91
+ | {
92
+ riskLevel: string;
93
+ reason: string;
94
+ scopeOptions: Array<{ pattern: string; label: string }>;
95
+ matchType: string;
96
+ }
97
+ | undefined;
98
+
89
99
  /** Spy on addRule to capture calls without replacing the real implementation. */
90
100
  let addRuleSpy: ReturnType<typeof spyOn> | undefined;
91
101
 
@@ -127,6 +137,7 @@ mock.module("../permissions/checker.js", () => ({
127
137
  ],
128
138
  generateScopeOptions: () =>
129
139
  scopeOptionsOverride ?? [{ label: "/tmp", scope: "/tmp" }],
140
+ getCachedAssessment: () => cachedAssessmentOverride,
130
141
  }));
131
142
 
132
143
  // Mock every export so downstream test files that dynamically import modules
@@ -196,6 +207,7 @@ describe("ToolExecutor allowedToolNames gating", () => {
196
207
  getToolOverride = undefined;
197
208
  checkResultOverride = undefined;
198
209
  checkFnOverride = undefined;
210
+ cachedAssessmentOverride = undefined;
199
211
  if (addRuleSpy) {
200
212
  addRuleSpy.mockRestore();
201
213
  addRuleSpy = undefined;
@@ -272,6 +284,7 @@ describe("ToolExecutor policy context plumbing", () => {
272
284
  getToolOverride = undefined;
273
285
  checkResultOverride = undefined;
274
286
  checkFnOverride = undefined;
287
+ cachedAssessmentOverride = undefined;
275
288
  if (addRuleSpy) {
276
289
  addRuleSpy.mockRestore();
277
290
  addRuleSpy = undefined;
@@ -309,6 +322,7 @@ describe("ToolExecutor policy context plumbing", () => {
309
322
  expect(result.isError).toBe(false);
310
323
  expect(lastCheckArgs).toBeDefined();
311
324
  expect(lastCheckArgs!.policyContext).toEqual({
325
+ conversationId: "conversation-1",
312
326
  executionContext: "conversation",
313
327
  ephemeralRules: undefined,
314
328
  executionTarget: "sandbox",
@@ -329,6 +343,7 @@ describe("ToolExecutor policy context plumbing", () => {
329
343
  expect(result.isError).toBe(false);
330
344
  expect(lastCheckArgs).toBeDefined();
331
345
  expect(lastCheckArgs!.policyContext).toEqual({
346
+ conversationId: "conversation-1",
332
347
  executionContext: "conversation",
333
348
  ephemeralRules: undefined,
334
349
  });
@@ -362,6 +377,7 @@ describe("ToolExecutor policy context plumbing", () => {
362
377
  expect(result.isError).toBe(false);
363
378
  expect(lastCheckArgs).toBeDefined();
364
379
  expect(lastCheckArgs!.policyContext).toEqual({
380
+ conversationId: "conversation-1",
365
381
  executionContext: "conversation",
366
382
  ephemeralRules: undefined,
367
383
  });
@@ -398,6 +414,7 @@ describe("ToolExecutor policy context plumbing", () => {
398
414
  expect(result.isError).toBe(false);
399
415
  expect(lastCheckArgs).toBeDefined();
400
416
  expect(lastCheckArgs!.policyContext).toEqual({
417
+ conversationId: "conversation-1",
401
418
  executionContext: "conversation",
402
419
  ephemeralRules: undefined,
403
420
  executionTarget: "host",
@@ -430,6 +447,7 @@ describe("ToolExecutor policy context plumbing", () => {
430
447
  expect(result.isError).toBe(false);
431
448
  expect(lastCheckArgs).toBeDefined();
432
449
  expect(lastCheckArgs!.policyContext).toEqual({
450
+ conversationId: "conversation-1",
433
451
  executionContext: "conversation",
434
452
  ephemeralRules: undefined,
435
453
  executionTarget: undefined,
@@ -749,6 +767,7 @@ describe("ToolExecutor strict mode + high-risk integration (PR 25)", () => {
749
767
  getToolOverride = undefined;
750
768
  checkResultOverride = undefined;
751
769
  checkFnOverride = undefined;
770
+ cachedAssessmentOverride = undefined;
752
771
  if (addRuleSpy) {
753
772
  addRuleSpy.mockRestore();
754
773
  addRuleSpy = undefined;
@@ -901,6 +920,7 @@ describe("ToolExecutor strict mode + high-risk integration (PR 25)", () => {
901
920
 
902
921
  expect(lastCheckArgs).toBeDefined();
903
922
  expect(lastCheckArgs!.policyContext).toEqual({
923
+ conversationId: "conversation-1",
904
924
  executionContext: "conversation",
905
925
  ephemeralRules: undefined,
906
926
  executionTarget: "sandbox",
@@ -1085,6 +1105,7 @@ describe("ToolExecutor strict mode + high-risk integration (PR 25)", () => {
1085
1105
 
1086
1106
  expect(lastCheckArgs).toBeDefined();
1087
1107
  expect(lastCheckArgs!.policyContext).toEqual({
1108
+ conversationId: "conversation-1",
1088
1109
  executionContext: "conversation",
1089
1110
  ephemeralRules: undefined,
1090
1111
  executionTarget: "sandbox",
@@ -1252,6 +1273,7 @@ describe("ToolExecutor baseline: allow rule auto-allows file_edit guardian perso
1252
1273
  getToolOverride = undefined;
1253
1274
  checkResultOverride = undefined;
1254
1275
  checkFnOverride = undefined;
1276
+ cachedAssessmentOverride = undefined;
1255
1277
  if (addRuleSpy) {
1256
1278
  addRuleSpy.mockRestore();
1257
1279
  addRuleSpy = undefined;
@@ -1369,6 +1391,7 @@ describe("ToolExecutor forcePromptSideEffects enforcement", () => {
1369
1391
  getToolOverride = undefined;
1370
1392
  checkResultOverride = undefined;
1371
1393
  checkFnOverride = undefined;
1394
+ cachedAssessmentOverride = undefined;
1372
1395
  promptCalled = false;
1373
1396
  if (addRuleSpy) {
1374
1397
  addRuleSpy.mockRestore();
@@ -2124,6 +2147,7 @@ describe("ToolExecutor persistent-allow lifecycle", () => {
2124
2147
  getToolOverride = undefined;
2125
2148
  checkResultOverride = undefined;
2126
2149
  checkFnOverride = undefined;
2150
+ cachedAssessmentOverride = undefined;
2127
2151
  if (addRuleSpy) {
2128
2152
  addRuleSpy.mockRestore();
2129
2153
  addRuleSpy = undefined;
@@ -2302,3 +2326,120 @@ describe("integration regressions — prompt payload (PR 11)", () => {
2302
2326
  expect(capturedScopes![0]).toHaveProperty("scope");
2303
2327
  });
2304
2328
  });
2329
+
2330
+ // ---------------------------------------------------------------------------
2331
+ // Risk metadata on ToolExecutionResult (PR 5 — scope-ladder-v1)
2332
+ // ---------------------------------------------------------------------------
2333
+
2334
+ describe("ToolExecutionResult includes risk metadata from classifier assessment", () => {
2335
+ beforeEach(() => {
2336
+ fakeToolResult = { content: "ok", isError: false };
2337
+ lastCheckArgs = undefined;
2338
+ getToolOverride = undefined;
2339
+ checkResultOverride = undefined;
2340
+ checkFnOverride = undefined;
2341
+ cachedAssessmentOverride = undefined;
2342
+ if (addRuleSpy) {
2343
+ addRuleSpy.mockRestore();
2344
+ addRuleSpy = undefined;
2345
+ }
2346
+ });
2347
+
2348
+ test("auto-approved tool result includes risk metadata when classifier assessment exists", async () => {
2349
+ cachedAssessmentOverride = {
2350
+ riskLevel: "medium",
2351
+ reason: "Writes to a file outside the workspace",
2352
+ scopeOptions: [
2353
+ { pattern: "file_write:/tmp/test.txt", label: "This file only" },
2354
+ { pattern: "file_write:/tmp/**", label: "Anything in tmp/" },
2355
+ ],
2356
+ matchType: "registry",
2357
+ };
2358
+
2359
+ const executor = new ToolExecutor(makePrompter());
2360
+ const result = await executor.execute(
2361
+ "file_read",
2362
+ { path: "README.md" },
2363
+ makeContext(),
2364
+ );
2365
+
2366
+ expect(result.isError).toBe(false);
2367
+ expect(result.riskLevel).toBe("medium");
2368
+ expect(result.riskReason).toBe("Writes to a file outside the workspace");
2369
+ expect(result.riskScopeOptions).toEqual([
2370
+ { pattern: "file_write:/tmp/test.txt", label: "This file only" },
2371
+ { pattern: "file_write:/tmp/**", label: "Anything in tmp/" },
2372
+ ]);
2373
+ });
2374
+
2375
+ test("tool result omits risk metadata when no classifier assessment exists (e.g. MCP tools)", async () => {
2376
+ // cachedAssessmentOverride is undefined (no classifier ran)
2377
+ const executor = new ToolExecutor(makePrompter());
2378
+ const result = await executor.execute(
2379
+ "file_read",
2380
+ { path: "README.md" },
2381
+ makeContext(),
2382
+ );
2383
+
2384
+ expect(result.isError).toBe(false);
2385
+ expect(result.riskLevel).toBeUndefined();
2386
+ expect(result.riskReason).toBeUndefined();
2387
+ expect(result.riskScopeOptions).toBeUndefined();
2388
+ });
2389
+
2390
+ test("denied tool result includes risk metadata", async () => {
2391
+ checkResultOverride = {
2392
+ decision: "deny",
2393
+ reason: "Blocked by deny rule",
2394
+ };
2395
+ cachedAssessmentOverride = {
2396
+ riskLevel: "high",
2397
+ reason: "Recursive force delete",
2398
+ scopeOptions: [{ pattern: "bash:rm -rf*", label: "rm -rf commands" }],
2399
+ matchType: "registry",
2400
+ };
2401
+
2402
+ const executor = new ToolExecutor(makePrompter());
2403
+ const result = await executor.execute(
2404
+ "bash",
2405
+ { command: "rm -rf /" },
2406
+ makeContext(),
2407
+ );
2408
+
2409
+ expect(result.isError).toBe(true);
2410
+ expect(result.riskLevel).toBe("high");
2411
+ expect(result.riskReason).toBe("Recursive force delete");
2412
+ expect(result.riskScopeOptions).toEqual([
2413
+ { pattern: "bash:rm -rf*", label: "rm -rf commands" },
2414
+ ]);
2415
+ });
2416
+
2417
+ test("prompted-then-approved tool result includes risk metadata", async () => {
2418
+ checkResultOverride = {
2419
+ decision: "prompt",
2420
+ reason: "Medium risk: requires approval",
2421
+ };
2422
+ cachedAssessmentOverride = {
2423
+ riskLevel: "medium",
2424
+ reason: "Package manager installation",
2425
+ scopeOptions: [
2426
+ { pattern: "bash:npm install*", label: "npm install commands" },
2427
+ { pattern: "bash:npm*", label: "All npm commands" },
2428
+ ],
2429
+ matchType: "registry",
2430
+ };
2431
+
2432
+ const executor = new ToolExecutor(makePrompter());
2433
+ const result = await executor.execute(
2434
+ "bash",
2435
+ { command: "npm install lodash" },
2436
+ makeContext(),
2437
+ );
2438
+
2439
+ expect(result.isError).toBe(false);
2440
+ expect(result.content).toBe("ok");
2441
+ expect(result.riskLevel).toBe("medium");
2442
+ expect(result.riskReason).toBe("Package manager installation");
2443
+ expect(result.riskScopeOptions).toHaveLength(2);
2444
+ });
2445
+ });
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Tests for the `toolResultTruncate` plugin pipeline (PR 17).
3
+ *
4
+ * Covers:
5
+ * - The default terminal delegates to `truncateToolResultText`, producing
6
+ * byte-for-byte identical output to calling the helper directly across
7
+ * short, long, and newline-bounded inputs (property-style).
8
+ * - The pipeline routes through `runPipeline` with the
9
+ * `DEFAULT_TIMEOUTS.toolResultTruncate` budget and returns a
10
+ * `{ content, truncated }` pair whose `truncated` flag matches whether
11
+ * the content actually changed.
12
+ * - Plugins registered alongside the default can short-circuit or decorate
13
+ * the terminal's output.
14
+ * - A user plugin registered AFTER the default still runs — the default
15
+ * plugin's middleware is a passthrough, so the onion composition cannot
16
+ * shadow late-registered middleware.
17
+ */
18
+
19
+ import { beforeEach, describe, expect, test } from "bun:test";
20
+
21
+ import {
22
+ MIN_KEEP_CHARS,
23
+ truncateToolResultText,
24
+ TRUNCATION_SUFFIX,
25
+ } from "../context/tool-result-truncation.js";
26
+ import type { TrustContext } from "../daemon/conversation-runtime-assembly.js";
27
+ import {
28
+ defaultToolResultTruncatePlugin,
29
+ defaultToolResultTruncateTerminal,
30
+ } from "../plugins/defaults/tool-result-truncate.js";
31
+ import { DEFAULT_TIMEOUTS, runPipeline } from "../plugins/pipeline.js";
32
+ import {
33
+ getMiddlewaresFor,
34
+ registerPlugin,
35
+ resetPluginRegistryForTests,
36
+ } from "../plugins/registry.js";
37
+ import {
38
+ type Middleware,
39
+ type ToolResultTruncateArgs,
40
+ type ToolResultTruncateResult,
41
+ type TurnContext,
42
+ } from "../plugins/types.js";
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Fixtures
46
+ // ---------------------------------------------------------------------------
47
+
48
+ const trust: TrustContext = {
49
+ sourceChannel: "vellum",
50
+ trustClass: "guardian",
51
+ };
52
+
53
+ function makeCtx(overrides: Partial<TurnContext> = {}): TurnContext {
54
+ return {
55
+ requestId: "req-test",
56
+ conversationId: "conv-test",
57
+ turnIndex: 0,
58
+ trust,
59
+ ...overrides,
60
+ };
61
+ }
62
+
63
+ describe("toolResultTruncate pipeline", () => {
64
+ beforeEach(() => {
65
+ resetPluginRegistryForTests();
66
+ });
67
+
68
+ // -------------------------------------------------------------------------
69
+ // Default terminal — isolated (no pipeline runner)
70
+ // -------------------------------------------------------------------------
71
+
72
+ describe("default terminal", () => {
73
+ test("passes short content through unchanged with truncated=false", () => {
74
+ const content = "hello world";
75
+ const result = defaultToolResultTruncateTerminal({
76
+ content,
77
+ maxChars: 100,
78
+ });
79
+ expect(result.content).toBe(content);
80
+ expect(result.truncated).toBe(false);
81
+ });
82
+
83
+ test("truncates oversize content and reports truncated=true", () => {
84
+ const content = "a".repeat(10_000);
85
+ const maxChars = 5_000;
86
+ const expected = truncateToolResultText(content, maxChars);
87
+ const result = defaultToolResultTruncateTerminal({ content, maxChars });
88
+ expect(result.content).toBe(expected);
89
+ expect(result.truncated).toBe(true);
90
+ expect(result.content).toContain(TRUNCATION_SUFFIX);
91
+ });
92
+
93
+ test("snaps to newline boundary identically to truncateToolResultText", () => {
94
+ const lines = Array.from(
95
+ { length: 1_000 },
96
+ (_, i) => `line ${i}: ${"x".repeat(20)}`,
97
+ ).join("\n");
98
+ const maxChars = 5_000;
99
+ const expected = truncateToolResultText(lines, maxChars);
100
+ const result = defaultToolResultTruncateTerminal({
101
+ content: lines,
102
+ maxChars,
103
+ });
104
+ expect(result.content).toBe(expected);
105
+ expect(result.truncated).toBe(true);
106
+ });
107
+
108
+ test("returns truncated=false when effectiveMax keeps the full text (maxChars < MIN_KEEP_CHARS case)", () => {
109
+ const textLength = MIN_KEEP_CHARS - TRUNCATION_SUFFIX.length - 10;
110
+ const content = "a".repeat(textLength);
111
+ const maxChars = 100;
112
+ const expected = truncateToolResultText(content, maxChars);
113
+ const result = defaultToolResultTruncateTerminal({ content, maxChars });
114
+ // Helper returns the original text unchanged in this case.
115
+ expect(expected).toBe(content);
116
+ expect(result.content).toBe(content);
117
+ expect(result.truncated).toBe(false);
118
+ });
119
+ });
120
+
121
+ // -------------------------------------------------------------------------
122
+ // End-to-end: default plugin routed through runPipeline
123
+ // -------------------------------------------------------------------------
124
+
125
+ describe("runPipeline with the default plugin registered", () => {
126
+ async function runDefault(
127
+ content: string,
128
+ maxChars: number,
129
+ ): Promise<ToolResultTruncateResult> {
130
+ registerPlugin(defaultToolResultTruncatePlugin);
131
+ const middlewares = getMiddlewaresFor("toolResultTruncate");
132
+ return runPipeline<ToolResultTruncateArgs, ToolResultTruncateResult>(
133
+ "toolResultTruncate",
134
+ middlewares,
135
+ async (args) => defaultToolResultTruncateTerminal(args),
136
+ { content, maxChars },
137
+ makeCtx(),
138
+ DEFAULT_TIMEOUTS.toolResultTruncate,
139
+ );
140
+ }
141
+
142
+ test("short content round-trip matches truncateToolResultText", async () => {
143
+ const content = "quick brown fox";
144
+ const maxChars = 200;
145
+ const expected = truncateToolResultText(content, maxChars);
146
+ const result = await runDefault(content, maxChars);
147
+ expect(result.content).toBe(expected);
148
+ expect(result.truncated).toBe(false);
149
+ });
150
+
151
+ test("long content round-trip matches truncateToolResultText", async () => {
152
+ const content = "z".repeat(50_000);
153
+ const maxChars = 10_000;
154
+ const expected = truncateToolResultText(content, maxChars);
155
+ const result = await runDefault(content, maxChars);
156
+ expect(result.content).toBe(expected);
157
+ expect(result.truncated).toBe(true);
158
+ expect(result.content).toContain(TRUNCATION_SUFFIX);
159
+ });
160
+
161
+ test("newline-bounded content round-trip matches truncateToolResultText", async () => {
162
+ const lines = Array.from(
163
+ { length: 500 },
164
+ (_, i) => `line ${i}: ${"y".repeat(40)}`,
165
+ ).join("\n");
166
+ const maxChars = 4_000;
167
+ const expected = truncateToolResultText(lines, maxChars);
168
+ const result = await runDefault(lines, maxChars);
169
+ expect(result.content).toBe(expected);
170
+ expect(result.truncated).toBe(true);
171
+ });
172
+
173
+ test("property test: default pipeline output equals direct truncateToolResultText across varied inputs", async () => {
174
+ // Deterministic pseudo-random over a fixed seed — bun's test runner
175
+ // doesn't ship a property-test library, so we hand-roll a tiny LCG
176
+ // that produces enough spread for a meaningful regression signal
177
+ // without introducing a dependency.
178
+ let seed = 0xc0ffee;
179
+ const rand = () => {
180
+ seed = (seed * 1664525 + 1013904223) & 0xffffffff;
181
+ return (seed >>> 0) / 0x100000000;
182
+ };
183
+ const alphabet = "abcdefghijklmnopqrstuvwxyz \n";
184
+
185
+ const cases: Array<{ content: string; maxChars: number }> = [];
186
+ for (let i = 0; i < 40; i++) {
187
+ // Lengths span short, boundary, and long relative to the maxChars
188
+ // budget so the property covers pass-through, newline-snap, and
189
+ // pure tail-drop paths.
190
+ const length = Math.floor(rand() * 20_000);
191
+ let content = "";
192
+ for (let j = 0; j < length; j++) {
193
+ content += alphabet[Math.floor(rand() * alphabet.length)];
194
+ }
195
+ const maxChars = 1_000 + Math.floor(rand() * 9_000);
196
+ cases.push({ content, maxChars });
197
+ }
198
+
199
+ // Register once outside the loop — registry is reset in `beforeEach`,
200
+ // so the per-case reset lives in the loop instead.
201
+ for (const { content, maxChars } of cases) {
202
+ resetPluginRegistryForTests();
203
+ registerPlugin(defaultToolResultTruncatePlugin);
204
+ const middlewares = getMiddlewaresFor("toolResultTruncate");
205
+ const result = await runPipeline<
206
+ ToolResultTruncateArgs,
207
+ ToolResultTruncateResult
208
+ >(
209
+ "toolResultTruncate",
210
+ middlewares,
211
+ async (args) => defaultToolResultTruncateTerminal(args),
212
+ { content, maxChars },
213
+ makeCtx(),
214
+ DEFAULT_TIMEOUTS.toolResultTruncate,
215
+ );
216
+ const expected = truncateToolResultText(content, maxChars);
217
+ expect(result.content).toBe(expected);
218
+ expect(result.truncated).toBe(expected !== content);
219
+ }
220
+ });
221
+ });
222
+
223
+ // -------------------------------------------------------------------------
224
+ // Middleware composition — an outer plugin can intercept / transform
225
+ // -------------------------------------------------------------------------
226
+
227
+ describe("middleware composition", () => {
228
+ test("an outer plugin can short-circuit the default with its own content", async () => {
229
+ const shortCircuit: Middleware<
230
+ ToolResultTruncateArgs,
231
+ ToolResultTruncateResult
232
+ > = async (_args, _next, _ctx) => {
233
+ return { content: "SUMMARY", truncated: true };
234
+ };
235
+ registerPlugin({
236
+ manifest: {
237
+ name: "short-circuit",
238
+ version: "1.0.0",
239
+ requires: {
240
+ pluginRuntime: "v1",
241
+ toolResultTruncateApi: "v1",
242
+ },
243
+ },
244
+ middleware: { toolResultTruncate: shortCircuit },
245
+ });
246
+ registerPlugin(defaultToolResultTruncatePlugin);
247
+
248
+ const middlewares = getMiddlewaresFor("toolResultTruncate");
249
+ const result = await runPipeline<
250
+ ToolResultTruncateArgs,
251
+ ToolResultTruncateResult
252
+ >(
253
+ "toolResultTruncate",
254
+ middlewares,
255
+ async (args) => defaultToolResultTruncateTerminal(args),
256
+ { content: "a".repeat(10_000), maxChars: 5_000 },
257
+ makeCtx(),
258
+ DEFAULT_TIMEOUTS.toolResultTruncate,
259
+ );
260
+
261
+ expect(result.content).toBe("SUMMARY");
262
+ expect(result.truncated).toBe(true);
263
+ });
264
+
265
+ test("an outer plugin can observe and mutate the default's output", async () => {
266
+ const prefixer: Middleware<
267
+ ToolResultTruncateArgs,
268
+ ToolResultTruncateResult
269
+ > = async (args, next, _ctx) => {
270
+ const inner = await next(args);
271
+ return { ...inner, content: `[wrapped] ${inner.content}` };
272
+ };
273
+ registerPlugin({
274
+ manifest: {
275
+ name: "prefixer",
276
+ version: "1.0.0",
277
+ requires: {
278
+ pluginRuntime: "v1",
279
+ toolResultTruncateApi: "v1",
280
+ },
281
+ },
282
+ middleware: { toolResultTruncate: prefixer },
283
+ });
284
+ registerPlugin(defaultToolResultTruncatePlugin);
285
+
286
+ const middlewares = getMiddlewaresFor("toolResultTruncate");
287
+ const content = "hello";
288
+ const result = await runPipeline<
289
+ ToolResultTruncateArgs,
290
+ ToolResultTruncateResult
291
+ >(
292
+ "toolResultTruncate",
293
+ middlewares,
294
+ async (args) => defaultToolResultTruncateTerminal(args),
295
+ { content, maxChars: 100 },
296
+ makeCtx(),
297
+ DEFAULT_TIMEOUTS.toolResultTruncate,
298
+ );
299
+
300
+ expect(result.content).toBe(`[wrapped] ${content}`);
301
+ expect(result.truncated).toBe(false);
302
+ });
303
+
304
+ test("user plugin registered AFTER the default still runs (no shadowing)", async () => {
305
+ // Production registration order: defaults load first via the
306
+ // side-effect imports in `defaults/index.ts`, then user plugins
307
+ // register on top (via `bootstrapPlugins()` or hot-reload). The
308
+ // user's middleware ends up at a deeper onion layer than the
309
+ // default. If the default's middleware were to bypass `next` and
310
+ // call the terminal directly, the user middleware would never run
311
+ // — this test guards against that regression.
312
+ registerPlugin(defaultToolResultTruncatePlugin);
313
+
314
+ let userMiddlewareRan = false;
315
+ const userMiddleware: Middleware<
316
+ ToolResultTruncateArgs,
317
+ ToolResultTruncateResult
318
+ > = async (args, next) => {
319
+ userMiddlewareRan = true;
320
+ return next(args);
321
+ };
322
+ registerPlugin({
323
+ manifest: {
324
+ name: "late-user-plugin",
325
+ version: "0.0.1",
326
+ requires: {
327
+ pluginRuntime: "v1",
328
+ toolResultTruncateApi: "v1",
329
+ },
330
+ },
331
+ middleware: { toolResultTruncate: userMiddleware },
332
+ });
333
+
334
+ const middlewares = getMiddlewaresFor("toolResultTruncate");
335
+ const content = "a".repeat(10_000);
336
+ const maxChars = 5_000;
337
+ const result = await runPipeline<
338
+ ToolResultTruncateArgs,
339
+ ToolResultTruncateResult
340
+ >(
341
+ "toolResultTruncate",
342
+ middlewares,
343
+ async (args) => defaultToolResultTruncateTerminal(args),
344
+ { content, maxChars },
345
+ makeCtx(),
346
+ DEFAULT_TIMEOUTS.toolResultTruncate,
347
+ );
348
+
349
+ expect(userMiddlewareRan).toBe(true);
350
+ // Terminal still runs after the user passthrough, so output matches
351
+ // the direct helper.
352
+ expect(result.content).toBe(truncateToolResultText(content, maxChars));
353
+ expect(result.truncated).toBe(true);
354
+ });
355
+ });
356
+ });