@vellumai/assistant 0.8.10 → 0.8.11-staging.1

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 (400) hide show
  1. package/bun.lock +62 -1
  2. package/docs/workspace-tools.md +196 -0
  3. package/examples/plugins/echo/README.md +3 -3
  4. package/knip.json +1 -0
  5. package/openapi.yaml +460 -128
  6. package/package.json +2 -1
  7. package/scripts/build-plugin-api.ts +299 -0
  8. package/src/__tests__/agent-loop-callsite-precedence.test.ts +7 -0
  9. package/src/__tests__/agent-loop-compaction-events.test.ts +197 -0
  10. package/src/__tests__/agent-loop-exit-reason.test.ts +93 -96
  11. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +2 -0
  12. package/src/__tests__/agent-loop-output-hooks.test.ts +274 -1
  13. package/src/__tests__/agent-loop-override-profile.test.ts +3 -0
  14. package/src/__tests__/agent-loop-provider-error-recording.test.ts +4 -0
  15. package/src/__tests__/agent-loop-thinking.test.ts +4 -0
  16. package/src/__tests__/agent-loop.test.ts +578 -5
  17. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +0 -1
  18. package/src/__tests__/approval-cascade.test.ts +1 -0
  19. package/src/__tests__/background-workers-disk-pressure.test.ts +0 -2
  20. package/src/__tests__/btw-routes.test.ts +0 -1
  21. package/src/__tests__/build-persisted-content.test.ts +75 -1
  22. package/src/__tests__/catalog-install-normalize.test.ts +141 -0
  23. package/src/__tests__/ces-startup-timeout.test.ts +60 -0
  24. package/src/__tests__/compaction-events.test.ts +1 -0
  25. package/src/__tests__/config-managed-gemini-defaults.test.ts +2 -46
  26. package/src/__tests__/context-overflow-reducer.test.ts +264 -124
  27. package/src/__tests__/context-window-manager-overflow-rung.test.ts +351 -0
  28. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  29. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +13 -5
  30. package/src/__tests__/conversation-agent-loop-overflow.test.ts +284 -455
  31. package/src/__tests__/conversation-agent-loop.test.ts +131 -551
  32. package/src/__tests__/conversation-app-control-instantiation.test.ts +13 -0
  33. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -0
  34. package/src/__tests__/conversation-fork-crud.test.ts +259 -0
  35. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  36. package/src/__tests__/conversation-lifecycle.test.ts +257 -1
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -0
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +38 -377
  39. package/src/__tests__/conversation-queue.test.ts +1 -39
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +119 -8
  41. package/src/__tests__/conversation-skill-tools.test.ts +491 -5
  42. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  43. package/src/__tests__/conversation-slash-unknown.test.ts +1 -0
  44. package/src/__tests__/conversation-speed-override.test.ts +1 -0
  45. package/src/__tests__/conversation-store.test.ts +74 -0
  46. package/src/__tests__/conversation-surfaces-app-control.test.ts +4 -1
  47. package/src/__tests__/conversation-tool-setup-attribution.test.ts +323 -0
  48. package/src/__tests__/conversation-tool-setup-tools-disabled.test.ts +34 -0
  49. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -0
  52. package/src/__tests__/corrected-target.test.ts +93 -0
  53. package/src/__tests__/credential-execution-feature-gates.test.ts +3 -5
  54. package/src/__tests__/credential-execution-tools.test.ts +23 -11
  55. package/src/__tests__/credential-security-invariants.test.ts +6 -1
  56. package/src/__tests__/db-schedule-syntax-migration.test.ts +80 -0
  57. package/src/__tests__/device-id.test.ts +70 -1
  58. package/src/__tests__/embedding-managed-proxy-selection.test.ts +6 -40
  59. package/src/__tests__/empty-response-hook.test.ts +242 -66
  60. package/src/__tests__/external-plugin-loader.test.ts +0 -31
  61. package/src/__tests__/get-skill-detail-audit.test.ts +43 -1
  62. package/src/__tests__/guardian-routing-invariants.test.ts +91 -0
  63. package/src/__tests__/history-repair-hook.test.ts +228 -3
  64. package/src/__tests__/host-app-control-proxy.test.ts +45 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +254 -9
  66. package/src/__tests__/identity-routes.test.ts +1 -0
  67. package/src/__tests__/image-recovery-hook.test.ts +387 -0
  68. package/src/__tests__/injector-chain.test.ts +5 -4
  69. package/src/__tests__/injector-v3-suppression.test.ts +373 -47
  70. package/src/__tests__/intent-routing.test.ts +7 -0
  71. package/src/__tests__/memory-retrieval-hook.test.ts +117 -15
  72. package/src/__tests__/notification-decision-strategy.test.ts +3 -3
  73. package/src/__tests__/oauth-store.test.ts +0 -85
  74. package/src/__tests__/{context-overflow-policy.test.ts → overflow-policy.test.ts} +1 -1
  75. package/src/__tests__/parallel-tool.benchmark.test.ts +4 -0
  76. package/src/__tests__/persist-unsendable-image-downscale.test.ts +29 -9
  77. package/src/__tests__/persist-unsendable-image.test.ts +4 -4
  78. package/src/__tests__/persistence-secret-redaction.test.ts +78 -0
  79. package/src/__tests__/plugin-bootstrap.test.ts +82 -73
  80. package/src/__tests__/plugin-tool-contribution.test.ts +7 -4
  81. package/src/__tests__/plugin-types.test.ts +0 -8
  82. package/src/__tests__/provider-catalog-visibility.test.ts +1 -9
  83. package/src/__tests__/prune-old-conversations-job.test.ts +99 -0
  84. package/src/__tests__/registry.test.ts +240 -1
  85. package/src/__tests__/require-fresh-approval.test.ts +3 -0
  86. package/src/__tests__/schedule-routes.test.ts +116 -1
  87. package/src/__tests__/schedule-store.test.ts +28 -0
  88. package/src/__tests__/schedule-tools.test.ts +94 -1
  89. package/src/__tests__/server-history-render.test.ts +39 -0
  90. package/src/__tests__/skill-projection-feature-flag.test.ts +13 -0
  91. package/src/__tests__/skill-projection.benchmark.test.ts +25 -7
  92. package/src/__tests__/skills.test.ts +202 -0
  93. package/src/__tests__/slim-skill-category.test.ts +195 -0
  94. package/src/__tests__/strip-memory-injections.test.ts +33 -39
  95. package/src/__tests__/test-support/tool-invocation-seed.ts +79 -0
  96. package/src/__tests__/title-generate-hook.test.ts +9 -7
  97. package/src/__tests__/tool-audit-listener.test.ts +264 -1
  98. package/src/__tests__/tool-error-hook.test.ts +4 -3
  99. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -0
  100. package/src/__tests__/tool-executor-lifecycle-events.test.ts +273 -0
  101. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  102. package/src/__tests__/tool-start-timestamp.test.ts +218 -0
  103. package/src/__tests__/tools-get-route.test.ts +202 -0
  104. package/src/__tests__/workspace-tool-loader.test.ts +319 -0
  105. package/src/__tests__/workspace-tools-watcher-flag.test.ts +70 -0
  106. package/src/agent/loop.ts +569 -319
  107. package/src/api/events/tool-result.ts +9 -0
  108. package/src/api/events/tool-use-start.ts +7 -0
  109. package/src/api/index.ts +10 -0
  110. package/src/api/responses/conversation-message.ts +135 -27
  111. package/src/api/responses/memory-v3-selection-log.ts +4 -4
  112. package/src/approvals/guardian-request-resolvers.ts +26 -0
  113. package/src/browser-session/backends/host-bridge.ts +29 -0
  114. package/src/browser-session/index.ts +1 -0
  115. package/src/browser-session/types.ts +5 -1
  116. package/src/cli/commands/__tests__/schedules.test.ts +62 -4
  117. package/src/cli/commands/__tests__/skills.test.ts +53 -0
  118. package/src/cli/commands/channel-verification-sessions.ts +6 -6
  119. package/src/cli/commands/inference-providers.ts +0 -8
  120. package/src/cli/commands/plugins.ts +2 -2
  121. package/src/cli/commands/schedules.ts +27 -4
  122. package/src/cli/commands/skills.ts +187 -146
  123. package/src/cli/commands/tools.ts +106 -0
  124. package/src/cli/lib/__tests__/install-from-github.test.ts +256 -328
  125. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +6 -2
  126. package/src/cli/lib/__tests__/plugin-details.test.ts +10 -16
  127. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +2 -2
  128. package/src/cli/lib/__tests__/search-plugins.test.ts +145 -240
  129. package/src/cli/lib/install-from-github.ts +187 -117
  130. package/src/cli/lib/plugin-catalog-cache.ts +9 -9
  131. package/src/cli/lib/plugin-details.ts +38 -68
  132. package/src/cli/lib/plugin-marketplace.ts +42 -14
  133. package/src/cli/lib/search-plugins.ts +29 -129
  134. package/src/cli/program.ts +2 -0
  135. package/src/config/bundled-skills/acp/SKILL.md +1 -0
  136. package/src/config/bundled-skills/app-builder/SKILL.md +1 -0
  137. package/src/config/bundled-skills/app-control/SKILL.md +1 -0
  138. package/src/config/bundled-skills/computer-use/SKILL.md +1 -0
  139. package/src/config/bundled-skills/contacts/SKILL.md +1 -0
  140. package/src/config/bundled-skills/document-editor/SKILL.md +1 -0
  141. package/src/config/bundled-skills/followups/SKILL.md +1 -0
  142. package/src/config/bundled-skills/image-studio/SKILL.md +1 -0
  143. package/src/config/bundled-skills/media-processing/SKILL.md +1 -0
  144. package/src/config/bundled-skills/messaging/SKILL.md +1 -0
  145. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -0
  146. package/src/config/bundled-skills/playbooks/SKILL.md +1 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +1 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +11 -3
  149. package/src/config/bundled-skills/sequences/SKILL.md +1 -0
  150. package/src/config/bundled-skills/settings/SKILL.md +1 -0
  151. package/src/config/bundled-skills/skill-management/SKILL.md +101 -1
  152. package/src/config/bundled-skills/subagent/SKILL.md +1 -0
  153. package/src/config/bundled-skills/transcribe/SKILL.md +1 -0
  154. package/src/config/env-registry.ts +23 -0
  155. package/src/config/feature-flag-registry.json +17 -81
  156. package/src/config/loader.ts +5 -22
  157. package/src/config/schema.ts +2 -0
  158. package/src/config/schemas/__tests__/compaction-logs.test.ts +56 -0
  159. package/src/config/schemas/__tests__/memory-v2.test.ts +0 -1
  160. package/src/config/schemas/__tests__/memory-v3.test.ts +61 -1
  161. package/src/config/schemas/compaction-logs.ts +79 -0
  162. package/src/config/schemas/memory-v2.ts +0 -8
  163. package/src/config/schemas/memory-v3.ts +104 -33
  164. package/src/config/seed-inference-profiles.ts +1 -1
  165. package/src/config/skills.ts +117 -47
  166. package/src/context/compactor.ts +11 -0
  167. package/src/context/strip-injections.ts +38 -4
  168. package/src/credential-execution/feature-gates.ts +0 -21
  169. package/src/credential-execution/startup-timeout.ts +32 -4
  170. package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +18 -0
  171. package/src/daemon/conversation-agent-loop-handlers.ts +140 -91
  172. package/src/daemon/conversation-agent-loop.ts +123 -658
  173. package/src/daemon/conversation-error.ts +6 -33
  174. package/src/daemon/conversation-lifecycle.ts +1 -1
  175. package/src/daemon/conversation-runtime-assembly.ts +183 -22
  176. package/src/daemon/conversation-skill-tools.ts +137 -8
  177. package/src/daemon/conversation-slash.ts +0 -14
  178. package/src/daemon/conversation-store.ts +2 -19
  179. package/src/daemon/conversation-tool-setup.ts +87 -1
  180. package/src/daemon/conversation.ts +119 -50
  181. package/src/daemon/external-plugins-bootstrap.ts +36 -95
  182. package/src/daemon/handlers/config-channels.ts +11 -2
  183. package/src/daemon/handlers/shared.ts +9 -1
  184. package/src/daemon/handlers/skills.ts +10 -4
  185. package/src/daemon/host-app-control-proxy.ts +72 -57
  186. package/src/daemon/host-browser-proxy.ts +117 -22
  187. package/src/daemon/lifecycle.ts +34 -5
  188. package/src/daemon/message-protocol.ts +0 -7
  189. package/src/daemon/message-types/schedules.ts +1 -0
  190. package/src/daemon/message-types/skills.ts +17 -0
  191. package/src/daemon/providers-setup.ts +3 -0
  192. package/src/daemon/server.ts +3 -3
  193. package/src/daemon/tool-setup-types.ts +9 -3
  194. package/src/daemon/trust-context.ts +23 -0
  195. package/src/daemon/workspace-tools-watcher.ts +324 -0
  196. package/src/events/tool-audit-listener.ts +78 -16
  197. package/src/events/tool-metrics-listener.ts +2 -5
  198. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +227 -0
  199. package/src/memory/__tests__/conversation-queries.test.ts +176 -0
  200. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +20 -32
  201. package/src/memory/compaction-log-writer-clickhouse.ts +418 -0
  202. package/src/memory/conversation-crud.ts +134 -3
  203. package/src/memory/conversation-queries.ts +64 -7
  204. package/src/memory/db-init.ts +14 -0
  205. package/src/memory/embedding-backend.test.ts +130 -1
  206. package/src/memory/embedding-backend.ts +79 -106
  207. package/src/memory/embedding-gemini.ts +5 -0
  208. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +12 -0
  209. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +19 -0
  210. package/src/memory/graph/conversation-graph-memory.ts +36 -25
  211. package/src/memory/graph/tool-handlers.ts +3 -0
  212. package/src/memory/job-handlers/cleanup.ts +3 -1
  213. package/src/memory/jobs-store.ts +0 -28
  214. package/src/memory/jobs-worker.ts +10 -22
  215. package/src/memory/memory-marker.ts +29 -0
  216. package/src/memory/memory-retrospective-startup-cleanup.ts +1 -1
  217. package/src/memory/migrations/268-add-memory-v3-selections.ts +6 -0
  218. package/src/memory/migrations/270-schedule-description.ts +36 -0
  219. package/src/memory/migrations/275-tool-invocations-add-skill-id.test.ts +81 -0
  220. package/src/memory/migrations/275-tool-invocations-add-skill-id.ts +20 -0
  221. package/src/memory/migrations/276-tool-invocations-created-at-id-index.test.ts +68 -0
  222. package/src/memory/migrations/276-tool-invocations-created-at-id-index.ts +20 -0
  223. package/src/memory/migrations/277-add-memory-v3-ever-injected.ts +29 -0
  224. package/src/memory/migrations/278-tool-invocations-telemetry-columns.test.ts +96 -0
  225. package/src/memory/migrations/278-tool-invocations-telemetry-columns.ts +39 -0
  226. package/src/memory/migrations/279-create-skill-loaded-events.test.ts +84 -0
  227. package/src/memory/migrations/279-create-skill-loaded-events.ts +26 -0
  228. package/src/memory/migrations/280-conversations-surfaced-at.test.ts +88 -0
  229. package/src/memory/migrations/280-conversations-surfaced-at.ts +24 -0
  230. package/src/memory/migrations/index.ts +10 -0
  231. package/src/memory/migrations/registry.ts +8 -0
  232. package/src/memory/schema/conversations.ts +16 -0
  233. package/src/memory/schema/infrastructure.ts +26 -0
  234. package/src/memory/skill-loaded-events-store.test.ts +160 -0
  235. package/src/memory/skill-loaded-events-store.ts +95 -0
  236. package/src/memory/tool-executed-events-store.test.ts +219 -0
  237. package/src/memory/tool-executed-events-store.ts +102 -0
  238. package/src/memory/tool-usage-store.ts +15 -3
  239. package/src/memory/v2/__tests__/consolidation-job.test.ts +117 -12
  240. package/src/memory/v2/__tests__/consolidation-prompt-flag-gating-guard.test.ts +189 -0
  241. package/src/memory/v2/__tests__/injected-block-slugs.test.ts +90 -0
  242. package/src/memory/v2/__tests__/page-store.test.ts +33 -0
  243. package/src/memory/v2/__tests__/prompts-consolidation.test.ts +88 -15
  244. package/src/memory/v2/activation-store.ts +50 -1
  245. package/src/memory/v2/consolidation-job.ts +113 -29
  246. package/src/memory/v2/injected-block-slugs.ts +79 -0
  247. package/src/memory/v2/injection.ts +6 -1
  248. package/src/memory/v2/prompts/consolidation.ts +414 -13
  249. package/src/memory/v2/router.ts +2 -28
  250. package/src/memory/v2/static-context.ts +1 -1
  251. package/src/memory/v2/types.ts +16 -0
  252. package/src/notifications/__tests__/copy-composer.test.ts +244 -0
  253. package/src/notifications/access-request-copy.ts +298 -0
  254. package/src/notifications/adapters/slack.ts +3 -3
  255. package/src/notifications/adapters/telegram.ts +2 -1
  256. package/src/notifications/copy-composer.ts +49 -267
  257. package/src/notifications/decision-engine.ts +16 -35
  258. package/src/notifications/home-feed-side-effect.ts +1 -6
  259. package/src/oauth/oauth-store.ts +0 -9
  260. package/src/permissions/checker.test.ts +83 -1
  261. package/src/permissions/checker.ts +25 -2
  262. package/src/permissions/gateway-threshold-reader.test.ts +182 -0
  263. package/src/permissions/gateway-threshold-reader.ts +80 -0
  264. package/src/platform/client.ts +1 -3
  265. package/src/platform/feature-gate.ts +3 -12
  266. package/src/plugin-api/constants.ts +4 -2
  267. package/src/plugin-api/index.ts +63 -11
  268. package/src/plugin-api/types.ts +236 -71
  269. package/src/plugins/defaults/compaction/compact.ts +66 -2
  270. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +240 -32
  271. package/src/plugins/defaults/compaction/corrected-target.ts +53 -0
  272. package/src/{daemon/context-overflow-policy.ts → plugins/defaults/compaction/overflow-policy.ts} +1 -1
  273. package/src/plugins/defaults/compaction/window-manager.ts +303 -1
  274. package/src/plugins/defaults/empty-response/hooks/post-model-call.ts +173 -0
  275. package/src/plugins/defaults/empty-response/hooks/stop.ts +11 -115
  276. package/src/plugins/defaults/empty-response/nudge-state-store.ts +46 -0
  277. package/src/plugins/defaults/history-repair/hooks/post-model-call.ts +50 -0
  278. package/src/plugins/defaults/history-repair/hooks/stop.ts +22 -0
  279. package/src/plugins/defaults/history-repair/repair-state-store.ts +51 -0
  280. package/src/plugins/defaults/history-repair/terminal.ts +39 -2
  281. package/src/plugins/defaults/image-recovery/detect.ts +25 -0
  282. package/src/plugins/defaults/image-recovery/hooks/post-model-call.ts +73 -0
  283. package/src/plugins/defaults/image-recovery/hooks/stop.ts +22 -0
  284. package/src/plugins/defaults/image-recovery/image-recovery-state-store.ts +48 -0
  285. package/src/plugins/defaults/image-recovery/package.json +14 -0
  286. package/src/{daemon/persist-unsendable-image.ts → plugins/defaults/image-recovery/recover.ts} +67 -14
  287. package/src/plugins/defaults/index.ts +71 -5
  288. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +76 -112
  289. package/src/plugins/defaults/memory-retrieval/hooks/{user-prompt-submit-temp.ts → user-prompt-submit.ts} +83 -74
  290. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +14 -8
  291. package/src/plugins/defaults/memory-retrieval/injectors.ts +2 -18
  292. package/src/plugins/defaults/memory-retrieval/package.json +14 -0
  293. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +1157 -0
  294. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +683 -0
  295. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +161 -140
  296. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +160 -0
  297. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +335 -316
  298. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +145 -53
  299. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +38 -1
  300. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +18 -8
  301. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +112 -71
  302. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +192 -51
  303. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +4 -16
  304. package/src/plugins/defaults/memory-v3-shadow/card.test.ts +173 -0
  305. package/src/plugins/defaults/memory-v3-shadow/card.ts +116 -0
  306. package/src/plugins/defaults/memory-v3-shadow/core-set.test.ts +104 -0
  307. package/src/plugins/defaults/memory-v3-shadow/core-set.ts +59 -0
  308. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.test.ts +305 -0
  309. package/src/plugins/defaults/memory-v3-shadow/ever-injected-store.ts +278 -0
  310. package/src/plugins/defaults/memory-v3-shadow/hot-set.test.ts +138 -0
  311. package/src/plugins/defaults/memory-v3-shadow/hot-set.ts +85 -0
  312. package/src/plugins/defaults/memory-v3-shadow/injector.ts +331 -24
  313. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +119 -13
  314. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +169 -114
  315. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +47 -16
  316. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +144 -66
  317. package/src/plugins/defaults/memory-v3-shadow/prune.test.ts +758 -0
  318. package/src/plugins/defaults/memory-v3-shadow/prune.ts +471 -0
  319. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +68 -16
  320. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +19 -12
  321. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +96 -43
  322. package/src/plugins/defaults/memory-v3-shadow/types.ts +34 -17
  323. package/src/plugins/defaults/title-generate/hooks/stop.ts +9 -11
  324. package/src/plugins/pipeline.ts +8 -5
  325. package/src/plugins/types.ts +5 -51
  326. package/src/providers/cache-control.ts +26 -0
  327. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +1 -2
  328. package/src/providers/model-catalog.ts +13 -1
  329. package/src/providers/openai/__tests__/tool-choice-mapping.test.ts +147 -0
  330. package/src/providers/openai/chat-completions-provider.ts +46 -0
  331. package/src/providers/openai/responses-provider.ts +45 -0
  332. package/src/providers/registry.ts +0 -8
  333. package/src/runtime/__tests__/agent-wake.test.ts +0 -1
  334. package/src/runtime/agent-wake.ts +13 -0
  335. package/src/runtime/routes/__tests__/acp-routes.test.ts +151 -0
  336. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +12 -50
  337. package/src/runtime/routes/__tests__/conversation-surface-routes.test.ts +322 -0
  338. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +0 -62
  339. package/src/runtime/routes/__tests__/plugins-routes.test.ts +31 -11
  340. package/src/runtime/routes/acp-routes.test.ts +106 -0
  341. package/src/runtime/routes/acp-routes.ts +248 -2
  342. package/src/runtime/routes/browser-tabs-routes.ts +1 -1
  343. package/src/runtime/routes/channel-verification-routes.ts +14 -5
  344. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +0 -14
  345. package/src/runtime/routes/consolidation-routes.ts +6 -82
  346. package/src/runtime/routes/conversation-list-routes.ts +6 -0
  347. package/src/runtime/routes/conversation-management-routes.ts +71 -0
  348. package/src/runtime/routes/identity-routes.ts +8 -0
  349. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +33 -34
  350. package/src/runtime/routes/inference-provider-connection-routes.ts +0 -45
  351. package/src/runtime/routes/plugins-routes.ts +34 -45
  352. package/src/runtime/routes/schedule-routes.ts +43 -5
  353. package/src/runtime/routes/settings-routes.ts +140 -15
  354. package/src/runtime/routes/skills-routes.ts +18 -6
  355. package/src/runtime/services/__tests__/conversation-serializer.test.ts +140 -0
  356. package/src/runtime/services/conversation-serializer.ts +38 -1
  357. package/src/runtime/verification-outbound-actions.ts +147 -2
  358. package/src/runtime/verification-templates.ts +29 -3
  359. package/src/schedule/schedule-store.ts +19 -0
  360. package/src/skills/catalog-install.ts +77 -13
  361. package/src/tasks/task-scheduler.ts +1 -0
  362. package/src/telemetry/types.ts +66 -1
  363. package/src/telemetry/usage-telemetry-reporter.test.ts +542 -13
  364. package/src/telemetry/usage-telemetry-reporter.ts +213 -20
  365. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +49 -2
  366. package/src/tools/browser/__tests__/browser-status.test.ts +29 -5
  367. package/src/tools/browser/browser-execution.ts +27 -9
  368. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +380 -4
  369. package/src/tools/browser/cdp-client/__tests__/host-bridge-cdp-client.test.ts +107 -0
  370. package/src/tools/browser/cdp-client/__tests__/types.test.ts +6 -1
  371. package/src/tools/browser/cdp-client/factory.ts +217 -17
  372. package/src/tools/browser/cdp-client/host-bridge-cdp-client.ts +67 -0
  373. package/src/tools/browser/cdp-client/types.ts +22 -2
  374. package/src/tools/credential-execution/make-authenticated-request.ts +2 -1
  375. package/src/tools/credential-execution/manage-secure-command-tool.ts +169 -164
  376. package/src/tools/credential-execution/run-authenticated-command.ts +2 -1
  377. package/src/tools/executor.ts +39 -7
  378. package/src/tools/registry.ts +387 -5
  379. package/src/tools/schedule/create.ts +16 -0
  380. package/src/tools/schedule/list.ts +12 -4
  381. package/src/tools/schedule/update.ts +12 -0
  382. package/src/tools/skills/load.ts +11 -6
  383. package/src/tools/terminal/safe-env.ts +2 -0
  384. package/src/tools/types.ts +65 -9
  385. package/src/tools/workspace-tools/loader.ts +673 -0
  386. package/src/usage/attribution.ts +28 -0
  387. package/src/util/device-id.ts +17 -3
  388. package/src/util/platform.ts +16 -0
  389. package/tsconfig.plugin-api.json +13 -0
  390. package/src/__tests__/plugin-external-api.test.ts +0 -68
  391. package/src/__tests__/plugin-skill-contribution.test.ts +0 -355
  392. package/src/daemon/message-types/browser.ts +0 -10
  393. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +0 -187
  394. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/eval-turns.json +0 -36
  395. package/src/plugins/defaults/memory-v3-shadow/__tests__/fixtures/live-turns.json +0 -37
  396. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-eviction.test.ts +0 -106
  397. package/src/plugins/defaults/memory-v3-shadow/__tests__/working-set-skeleton.test.ts +0 -44
  398. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +0 -91
  399. package/src/plugins/external-api.ts +0 -114
  400. package/src/plugins/plugin-skill-contributions.ts +0 -292
@@ -1,9 +1,21 @@
1
- import { describe, expect, test } from "bun:test";
1
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
+
3
+ // Toggle for the collectUsageData opt-out gate the listener consults when
4
+ // populating the telemetry columns.
5
+ let collectUsageData = true;
6
+
7
+ mock.module("../config/loader.js", () => ({
8
+ getConfig: () => ({ collectUsageData }),
9
+ }));
2
10
 
3
11
  import { createToolAuditListener } from "../events/tool-audit-listener.js";
4
12
  import type { ToolInvocationRecord } from "../memory/tool-usage-store.js";
5
13
 
6
14
  describe("tool audit listener", () => {
15
+ beforeEach(() => {
16
+ collectUsageData = true;
17
+ });
18
+
7
19
  test("records executed events with truncated output", () => {
8
20
  const records: ToolInvocationRecord[] = [];
9
21
  const listener = createToolAuditListener((record) => records.push(record));
@@ -152,5 +164,256 @@ describe("tool audit listener", () => {
152
164
  expect(records).toHaveLength(1);
153
165
  expect(records[0].result).toBe("error: boom");
154
166
  expect(records[0].decision).toBe("error");
167
+ expect(records[0].argBytes).toBe(
168
+ Buffer.byteLength(JSON.stringify({ path: "/tmp/secret" }), "utf8"),
169
+ );
170
+ expect(records[0].resultBytes).toBe(Buffer.byteLength("error: boom"));
171
+ });
172
+
173
+ test("records byte sizes from the full payloads, before truncation and redaction", () => {
174
+ const records: ToolInvocationRecord[] = [];
175
+ const listener = createToolAuditListener((record) => records.push(record));
176
+
177
+ listener({
178
+ type: "executed",
179
+ toolName: "file_read",
180
+ input: { path: "/tmp/a" },
181
+ workingDir: "/tmp",
182
+ conversationId: "conv-bytes",
183
+ riskLevel: "low",
184
+ decision: "allow",
185
+ durationMs: 12,
186
+ // 1200 chars: the stored result is capped at 1000 but the size must
187
+ // reflect the full payload. "é" is 1 char / 2 utf8 bytes, proving
188
+ // byte (not char) accounting.
189
+ result: { content: "é".repeat(1200), isError: false },
190
+ });
191
+
192
+ expect(records).toHaveLength(1);
193
+ expect(records[0].result).toHaveLength(1000);
194
+ expect(records[0].argBytes).toBe(
195
+ Buffer.byteLength(JSON.stringify({ path: "/tmp/a" }), "utf8"),
196
+ );
197
+ expect(records[0].resultBytes).toBe(2400);
198
+ });
199
+
200
+ test("prefers the executor-stamped raw inputBytes over sizing the (sanitized) event input", () => {
201
+ const records: ToolInvocationRecord[] = [];
202
+ const listener = createToolAuditListener((record) => records.push(record));
203
+
204
+ // The executor sanitizes event.input before listeners run and stamps
205
+ // inputBytes from the RAW input — the listener must report that size,
206
+ // not the redacted payload's.
207
+ const rawInput = { path: "/tmp/a", token: "t-1" };
208
+ const rawSize = Buffer.byteLength(JSON.stringify(rawInput), "utf8");
209
+ const sanitizedInput = { path: "/tmp/a", token: "<redacted />" };
210
+
211
+ listener({
212
+ type: "executed",
213
+ toolName: "file_read",
214
+ input: sanitizedInput,
215
+ inputBytes: rawSize,
216
+ workingDir: "/tmp",
217
+ conversationId: "conv-raw-bytes",
218
+ riskLevel: "low",
219
+ decision: "allow",
220
+ durationMs: 4,
221
+ result: { content: "ok", isError: false },
222
+ });
223
+ listener({
224
+ type: "error",
225
+ toolName: "file_read",
226
+ input: sanitizedInput,
227
+ inputBytes: rawSize,
228
+ workingDir: "/tmp",
229
+ conversationId: "conv-raw-bytes",
230
+ riskLevel: "low",
231
+ decision: "error",
232
+ durationMs: 4,
233
+ errorMessage: "boom",
234
+ isExpected: false,
235
+ errorCategory: "tool_failure",
236
+ });
237
+
238
+ expect(records).toHaveLength(2);
239
+ for (const record of records) {
240
+ expect(record.argBytes).toBe(rawSize);
241
+ expect(record.argBytes).not.toBe(
242
+ Buffer.byteLength(JSON.stringify(sanitizedInput), "utf8"),
243
+ );
244
+ }
245
+ });
246
+
247
+ test("prefers the executor-stamped raw resultBytes over sizing the (sanitized) event result", () => {
248
+ const records: ToolInvocationRecord[] = [];
249
+ const listener = createToolAuditListener((record) => records.push(record));
250
+
251
+ // For sensitive-output tools, the executor sanitizes result.content
252
+ // before listeners run and stamps resultBytes from the RAW content —
253
+ // the listener must report that size, not the placeholder-rewritten
254
+ // payload's.
255
+ const sanitizedContent = "code: VELLUM_ASSISTANT_INVITE_CODE_AB12CD34";
256
+ const rawSize = 4096;
257
+
258
+ listener({
259
+ type: "executed",
260
+ toolName: "create_invite",
261
+ input: { count: 1 },
262
+ workingDir: "/tmp",
263
+ conversationId: "conv-raw-result-bytes",
264
+ riskLevel: "low",
265
+ decision: "allow",
266
+ durationMs: 4,
267
+ result: { content: sanitizedContent, isError: false },
268
+ resultBytes: rawSize,
269
+ });
270
+
271
+ expect(records).toHaveLength(1);
272
+ expect(records[0].resultBytes).toBe(rawSize);
273
+ expect(records[0].resultBytes).not.toBe(
274
+ Buffer.byteLength(sanitizedContent, "utf8"),
275
+ );
276
+ });
277
+
278
+ test("persists NULL telemetry columns when usage data collection is opted out", () => {
279
+ collectUsageData = false;
280
+ const records: ToolInvocationRecord[] = [];
281
+ const listener = createToolAuditListener((record) => records.push(record));
282
+
283
+ const attribution = {
284
+ callSite: "mainAgent" as const,
285
+ activeProfile: "balanced",
286
+ overrideProfile: null,
287
+ callSiteProfile: null,
288
+ appliedProfile: "balanced",
289
+ profileSource: "active" as const,
290
+ resolvedProvider: "anthropic",
291
+ resolvedModel: "model-a",
292
+ resolvedMixArm: null,
293
+ };
294
+ listener({
295
+ type: "executed",
296
+ toolName: "file_read",
297
+ input: { path: "/tmp/a" },
298
+ inputBytes: 42,
299
+ resultBytes: 99,
300
+ workingDir: "/tmp",
301
+ conversationId: "conv-opt-out",
302
+ riskLevel: "low",
303
+ decision: "allow",
304
+ durationMs: 12,
305
+ result: { content: "ok", isError: false },
306
+ attribution,
307
+ });
308
+ listener({
309
+ type: "error",
310
+ toolName: "file_read",
311
+ input: { path: "/tmp/b" },
312
+ inputBytes: 42,
313
+ workingDir: "/tmp",
314
+ conversationId: "conv-opt-out",
315
+ riskLevel: "low",
316
+ decision: "error",
317
+ durationMs: 9,
318
+ errorMessage: "boom",
319
+ isExpected: false,
320
+ errorCategory: "tool_failure",
321
+ attribution,
322
+ });
323
+
324
+ // Telemetry columns are NULL — the tool_executed projection's
325
+ // arg_bytes IS NOT NULL filter makes these rows permanently
326
+ // unreportable, even from a zero watermark.
327
+ expect(records).toHaveLength(2);
328
+ for (const record of records) {
329
+ expect(record.argBytes).toBeNull();
330
+ expect(record.resultBytes).toBeNull();
331
+ expect(record.provider).toBeNull();
332
+ expect(record.model).toBeNull();
333
+ expect(record.inferenceProfile).toBeNull();
334
+ expect(record.inferenceProfileSource).toBeNull();
335
+ }
336
+ // The audit row itself is unaffected by the opt-out.
337
+ expect(records[0]).toMatchObject({
338
+ conversationId: "conv-opt-out",
339
+ toolName: "file_read",
340
+ input: JSON.stringify({ path: "/tmp/a" }),
341
+ result: "ok",
342
+ decision: "allow",
343
+ durationMs: 12,
344
+ });
345
+ expect(records[1]).toMatchObject({
346
+ result: "error: boom",
347
+ decision: "error",
348
+ durationMs: 9,
349
+ });
350
+ });
351
+
352
+ test("maps attribution onto executed records and leaves denied records null", () => {
353
+ const records: ToolInvocationRecord[] = [];
354
+ const listener = createToolAuditListener((record) => records.push(record));
355
+
356
+ listener({
357
+ type: "executed",
358
+ toolName: "file_read",
359
+ input: { path: "/tmp/a" },
360
+ workingDir: "/tmp",
361
+ conversationId: "conv-attr",
362
+ riskLevel: "low",
363
+ decision: "allow",
364
+ durationMs: 12,
365
+ result: { content: "ok", isError: false },
366
+ attribution: {
367
+ callSite: "mainAgent",
368
+ activeProfile: "balanced",
369
+ overrideProfile: null,
370
+ callSiteProfile: null,
371
+ appliedProfile: "balanced",
372
+ profileSource: "active",
373
+ resolvedProvider: "anthropic",
374
+ resolvedModel: "model-a",
375
+ resolvedMixArm: null,
376
+ },
377
+ });
378
+ listener({
379
+ type: "executed",
380
+ toolName: "file_read",
381
+ input: { path: "/tmp/b" },
382
+ workingDir: "/tmp",
383
+ conversationId: "conv-attr",
384
+ riskLevel: "low",
385
+ decision: "allow",
386
+ durationMs: 3,
387
+ result: { content: "ok", isError: false },
388
+ attribution: null,
389
+ });
390
+ listener({
391
+ type: "permission_denied",
392
+ toolName: "bash",
393
+ input: { command: "rm -rf /tmp" },
394
+ workingDir: "/tmp",
395
+ conversationId: "conv-attr",
396
+ riskLevel: "high",
397
+ decision: "deny",
398
+ reason: "Permission denied by user",
399
+ durationMs: 1,
400
+ });
401
+
402
+ expect(records).toHaveLength(3);
403
+ expect(records[0]).toMatchObject({
404
+ provider: "anthropic",
405
+ model: "model-a",
406
+ inferenceProfile: "balanced",
407
+ inferenceProfileSource: "active",
408
+ });
409
+ expect(records[1]).toMatchObject({
410
+ provider: null,
411
+ model: null,
412
+ inferenceProfile: null,
413
+ inferenceProfileSource: null,
414
+ });
415
+ expect(records[2].provider).toBeUndefined();
416
+ expect(records[2].argBytes).toBeUndefined();
417
+ expect(records[2].resultBytes).toBeUndefined();
155
418
  });
156
419
  });
@@ -78,6 +78,7 @@ function makeCtx(
78
78
  conversationId: "conv-tool-error-test",
79
79
  toolResponse,
80
80
  messages,
81
+ additionalContext: null,
81
82
  maxInputTokens: 10_000,
82
83
  logger: noopLogger,
83
84
  };
@@ -139,7 +140,7 @@ describe("tool-error post-tool-use hook — direct", () => {
139
140
  await postToolUse(ctx);
140
141
 
141
142
  // THEN no coaching is surfaced and the content is left untouched.
142
- expect(ctx.additionalContext).toBeUndefined();
143
+ expect(ctx.additionalContext).toBeNull();
143
144
  expect(ctx.toolResponse.content).toBe(BASE_CONTENT);
144
145
  });
145
146
 
@@ -175,7 +176,7 @@ describe("tool-error post-tool-use hook — direct", () => {
175
176
 
176
177
  // THEN no coaching is surfaced (the error is likely unrecoverable) and the
177
178
  // result is left untouched.
178
- expect(ctx.additionalContext).toBeUndefined();
179
+ expect(ctx.additionalContext).toBeNull();
179
180
  expect(ctx.toolResponse.content).toBe(BASE_CONTENT);
180
181
  });
181
182
 
@@ -251,7 +252,7 @@ describe("tool-error post-tool-use hook — via runHook", () => {
251
252
  test("default hook runs before a later-registered user hook", async () => {
252
253
  // GIVEN the default plugin is registered first, then a user plugin whose
253
254
  // hook records the additionalContext it observes.
254
- let observed: string | undefined;
255
+ let observed: string | null | undefined;
255
256
  registerPlugin(defaultToolErrorPlugin);
256
257
  registerPlugin({
257
258
  manifest: { name: "observer-plugin", version: "0.0.1" },
@@ -75,6 +75,7 @@ mock.module("../util/platform.js", () => ({
75
75
 
76
76
  mock.module("../permissions/gateway-threshold-reader.js", () => ({
77
77
  getAutoApproveThreshold: async () => undefined,
78
+ refreshAutoApproveThreshold: async () => null,
78
79
  }));
79
80
 
80
81
  mock.module("../permissions/workspace-policy.js", () => ({
@@ -4,6 +4,7 @@ import type {
4
4
  ToolExecutionResult,
5
5
  ToolLifecycleEvent,
6
6
  } from "../tools/types.js";
7
+ import type { UsageAttributionSnapshot } from "../usage/attribution.js";
7
8
 
8
9
  const mockConfig = {
9
10
  provider: "anthropic",
@@ -33,6 +34,9 @@ const mockConfig = {
33
34
  permissions: {
34
35
  mode: "workspace" as const,
35
36
  },
37
+ // The audit listener nulls the telemetry columns when this is false; the
38
+ // end-to-end listener tests below assert the opted-in sizing behavior.
39
+ collectUsageData: true,
36
40
  };
37
41
 
38
42
  let checkerDecision: "allow" | "prompt" | "deny" = "allow";
@@ -515,6 +519,275 @@ describe("ToolExecutor lifecycle events", () => {
515
519
  expect(executed.executionTarget).toBe("sandbox");
516
520
  });
517
521
 
522
+ // ── attribution forwarding tests ──────────────────────────
523
+
524
+ // Uses a non-main call site (voice turn) so these tests also prove the
525
+ // executor forwards the snapshot verbatim — non-main turns must not be
526
+ // rewritten to the main agent's attribution.
527
+ const testAttribution: UsageAttributionSnapshot = {
528
+ callSite: "callAgent",
529
+ activeProfile: "balanced",
530
+ overrideProfile: null,
531
+ callSiteProfile: "voice-profile",
532
+ appliedProfile: "voice-profile",
533
+ profileSource: "call_site",
534
+ resolvedProvider: "anthropic",
535
+ resolvedModel: "test-model",
536
+ resolvedMixArm: null,
537
+ };
538
+
539
+ test("forwards context.attribution into the executed lifecycle event", async () => {
540
+ const events: ToolLifecycleEvent[] = [];
541
+ const executor = new ToolExecutor(makePrompter());
542
+
543
+ await executor.execute(
544
+ "file_read",
545
+ { path: "README.md" },
546
+ makeContext(events, { attribution: testAttribution }),
547
+ );
548
+
549
+ const executed = events.find((event) => event.type === "executed");
550
+ if (executed?.type !== "executed")
551
+ throw new Error("Expected executed event");
552
+ expect(executed.attribution).toEqual(testAttribution);
553
+ });
554
+
555
+ test("forwards context.attribution into the error lifecycle event", async () => {
556
+ toolThrow = new Error("boom");
557
+
558
+ const events: ToolLifecycleEvent[] = [];
559
+ const executor = new ToolExecutor(makePrompter());
560
+
561
+ await executor.execute(
562
+ "file_read",
563
+ {},
564
+ makeContext(events, { attribution: testAttribution }),
565
+ );
566
+
567
+ const errorEvent = events.find((event) => event.type === "error");
568
+ if (errorEvent?.type !== "error") throw new Error("Expected error event");
569
+ expect(errorEvent.attribution).toEqual(testAttribution);
570
+ });
571
+
572
+ test("missing context.attribution yields null on the executed event without throwing", async () => {
573
+ const events: ToolLifecycleEvent[] = [];
574
+ const executor = new ToolExecutor(makePrompter());
575
+
576
+ const result = await executor.execute(
577
+ "file_read",
578
+ { path: "README.md" },
579
+ makeContext(events),
580
+ );
581
+
582
+ expect(result).toMatchObject({ content: "ok", isError: false });
583
+ const executed = events.find((event) => event.type === "executed");
584
+ if (executed?.type !== "executed")
585
+ throw new Error("Expected executed event");
586
+ expect(executed.attribution).toBeNull();
587
+ });
588
+
589
+ test("missing context.attribution yields null on the error event without throwing", async () => {
590
+ toolThrow = new Error("boom");
591
+
592
+ const events: ToolLifecycleEvent[] = [];
593
+ const executor = new ToolExecutor(makePrompter());
594
+
595
+ const result = await executor.execute("file_read", {}, makeContext(events));
596
+
597
+ expect(result.isError).toBe(true);
598
+ const errorEvent = events.find((event) => event.type === "error");
599
+ if (errorEvent?.type !== "error") throw new Error("Expected error event");
600
+ expect(errorEvent.attribution).toBeNull();
601
+ });
602
+
603
+ test("stamps attribution on pre-execution gate error events (unknown tool)", async () => {
604
+ const events: ToolLifecycleEvent[] = [];
605
+ const executor = new ToolExecutor(makePrompter());
606
+
607
+ const result = await executor.execute(
608
+ "unknown_tool",
609
+ { test: true },
610
+ makeContext(events, { attribution: testAttribution }),
611
+ );
612
+
613
+ expect(result.isError).toBe(true);
614
+ const errorEvent = events.find((event) => event.type === "error");
615
+ if (errorEvent?.type !== "error") throw new Error("Expected error event");
616
+ expect(errorEvent.errorMessage).toContain("Unknown tool: unknown_tool");
617
+ expect(errorEvent.attribution).toEqual(testAttribution);
618
+ });
619
+
620
+ test("missing attribution yields null on pre-execution gate error events", async () => {
621
+ const events: ToolLifecycleEvent[] = [];
622
+ const executor = new ToolExecutor(makePrompter());
623
+
624
+ const result = await executor.execute(
625
+ "unknown_tool",
626
+ { test: true },
627
+ makeContext(events),
628
+ );
629
+
630
+ expect(result.isError).toBe(true);
631
+ const errorEvent = events.find((event) => event.type === "error");
632
+ if (errorEvent?.type !== "error") throw new Error("Expected error event");
633
+ expect(errorEvent.attribution).toBeNull();
634
+ });
635
+
636
+ test("stamps attribution on the aborted pre-execution gate error event", async () => {
637
+ const events: ToolLifecycleEvent[] = [];
638
+ const executor = new ToolExecutor(makePrompter());
639
+ const controller = new AbortController();
640
+ controller.abort();
641
+
642
+ const result = await executor.execute(
643
+ "file_read",
644
+ { path: "README.md" },
645
+ makeContext(events, {
646
+ attribution: testAttribution,
647
+ signal: controller.signal,
648
+ }),
649
+ );
650
+
651
+ expect(result).toEqual({ content: "Cancelled", isError: true });
652
+ const errorEvent = events.find((event) => event.type === "error");
653
+ if (errorEvent?.type !== "error") throw new Error("Expected error event");
654
+ expect(errorEvent.errorMessage).toBe("Cancelled");
655
+ expect(errorEvent.attribution).toEqual(testAttribution);
656
+ });
657
+
658
+ // ── raw input byte sizing tests ───────────────────────────
659
+
660
+ test("stamps inputBytes from the raw input even when sanitization redacts fields", async () => {
661
+ const events: ToolLifecycleEvent[] = [];
662
+ const executor = new ToolExecutor(makePrompter());
663
+
664
+ const rawInput = { path: "README.md", token: "t-1" };
665
+ const rawSize = Buffer.byteLength(JSON.stringify(rawInput), "utf8");
666
+
667
+ await executor.execute("file_read", rawInput, makeContext(events));
668
+
669
+ const executed = events.find((event) => event.type === "executed");
670
+ if (executed?.type !== "executed")
671
+ throw new Error("Expected executed event");
672
+ // The event input is sanitized, but the size reflects the raw payload.
673
+ expect(executed.input.token).toBe("<redacted />");
674
+ expect(executed.inputBytes).toBe(rawSize);
675
+ expect(executed.inputBytes).not.toBe(
676
+ Buffer.byteLength(JSON.stringify(executed.input), "utf8"),
677
+ );
678
+ });
679
+
680
+ test("stamps inputBytes from the raw input on error events", async () => {
681
+ toolThrow = new Error("boom");
682
+
683
+ const events: ToolLifecycleEvent[] = [];
684
+ const executor = new ToolExecutor(makePrompter());
685
+
686
+ const rawInput = { path: "README.md", api_key: "k-1" };
687
+
688
+ await executor.execute("file_read", rawInput, makeContext(events));
689
+
690
+ const errorEvent = events.find((event) => event.type === "error");
691
+ if (errorEvent?.type !== "error") throw new Error("Expected error event");
692
+ expect(errorEvent.input.api_key).toBe("<redacted />");
693
+ expect(errorEvent.inputBytes).toBe(
694
+ Buffer.byteLength(JSON.stringify(rawInput), "utf8"),
695
+ );
696
+ });
697
+
698
+ test("stamps resultBytes from the raw content before sensitive-output sanitization", async () => {
699
+ // Directive stripping + placeholder substitution shrink the content the
700
+ // lifecycle event carries; the stamped size must reflect the raw output.
701
+ const rawContent =
702
+ 'Your invite: <vellum-sensitive-output kind="invite_code" value="SECRET-CODE-123" /> use SECRET-CODE-123';
703
+ fakeToolResult = { content: rawContent, isError: false };
704
+
705
+ const events: ToolLifecycleEvent[] = [];
706
+ const executor = new ToolExecutor(makePrompter());
707
+
708
+ await executor.execute("file_read", { path: "a" }, makeContext(events));
709
+
710
+ const executed = events.find((event) => event.type === "executed");
711
+ if (executed?.type !== "executed")
712
+ throw new Error("Expected executed event");
713
+ // The event carries the sanitized content...
714
+ expect(executed.result.content).not.toContain("SECRET-CODE-123");
715
+ // ...but the stamped size is the raw pre-sanitization byte length.
716
+ expect(executed.resultBytes).toBe(Buffer.byteLength(rawContent, "utf8"));
717
+ expect(executed.resultBytes).not.toBe(
718
+ Buffer.byteLength(executed.result.content, "utf8"),
719
+ );
720
+ });
721
+
722
+ test("stamps resultBytes for non-sensitive results too (raw equals emitted content)", async () => {
723
+ const events: ToolLifecycleEvent[] = [];
724
+ const executor = new ToolExecutor(makePrompter());
725
+
726
+ await executor.execute("file_read", { path: "a" }, makeContext(events));
727
+
728
+ const executed = events.find((event) => event.type === "executed");
729
+ if (executed?.type !== "executed")
730
+ throw new Error("Expected executed event");
731
+ expect(executed.result.content).toBe("ok");
732
+ expect(executed.resultBytes).toBe(Buffer.byteLength("ok", "utf8"));
733
+ });
734
+
735
+ test("audit listener records result_bytes from the raw pre-sanitization output", async () => {
736
+ const { createToolAuditListener } =
737
+ await import("../events/tool-audit-listener.js");
738
+ const records: Array<{ resultBytes?: number | null; result: string }> = [];
739
+ const executor = new ToolExecutor(makePrompter());
740
+
741
+ const rawContent =
742
+ 'Code: <vellum-sensitive-output kind="invite_code" value="SECRET-CODE-456" />SECRET-CODE-456';
743
+ fakeToolResult = { content: rawContent, isError: false };
744
+
745
+ await executor.execute(
746
+ "file_read",
747
+ { path: "a" },
748
+ {
749
+ workingDir: "/tmp/project",
750
+ conversationId: "conversation-1",
751
+ trustClass: "guardian" as const,
752
+ onToolLifecycleEvent: createToolAuditListener((record) =>
753
+ records.push(record),
754
+ ),
755
+ },
756
+ );
757
+
758
+ expect(records).toHaveLength(1);
759
+ expect(records[0].resultBytes).toBe(Buffer.byteLength(rawContent, "utf8"));
760
+ // The stored result column is still the sanitized payload.
761
+ expect(records[0].result).not.toContain("SECRET-CODE-456");
762
+ });
763
+
764
+ test("audit listener records arg_bytes equal to the raw serialized input size", async () => {
765
+ // End-to-end: executor sanitization must not shrink the recorded
766
+ // arg_bytes — the audit row sizes the raw pre-redaction input.
767
+ const { createToolAuditListener } =
768
+ await import("../events/tool-audit-listener.js");
769
+ const records: Array<{ argBytes?: number | null; input: string }> = [];
770
+ const executor = new ToolExecutor(makePrompter());
771
+
772
+ const rawInput = { path: "README.md", token: "t-1" };
773
+
774
+ await executor.execute("file_read", rawInput, {
775
+ workingDir: "/tmp/project",
776
+ conversationId: "conversation-1",
777
+ trustClass: "guardian" as const,
778
+ onToolLifecycleEvent: createToolAuditListener((record) =>
779
+ records.push(record),
780
+ ),
781
+ });
782
+
783
+ expect(records).toHaveLength(1);
784
+ expect(records[0].argBytes).toBe(
785
+ Buffer.byteLength(JSON.stringify(rawInput), "utf8"),
786
+ );
787
+ // The stored input column is still the redacted payload.
788
+ expect(records[0].input).not.toContain("t-1");
789
+ });
790
+
518
791
  test("skill tool with sandbox execution_target resolves to sandbox executionTarget", async () => {
519
792
  const events: ToolLifecycleEvent[] = [];
520
793
  const executor = new ToolExecutor(makePrompter());
@@ -47,6 +47,7 @@ function makeCtx(content: string): PostToolUseContext {
47
47
  conversationId: "conv-test",
48
48
  toolResponse: makeToolResponse(content),
49
49
  messages: [],
50
+ additionalContext: null,
50
51
  maxInputTokens: MAX_INPUT_TOKENS,
51
52
  logger: noopLogger,
52
53
  };