@vellumai/assistant 0.11.1 → 0.11.2-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. package/Dockerfile +1 -3
  2. package/README.md +1 -1
  3. package/eslint.config.mjs +28 -8
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  12. package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  13. package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
  14. package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
  15. package/openapi.yaml +292 -5
  16. package/package.json +1 -1
  17. package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
  18. package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
  19. package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
  20. package/src/__tests__/compaction-events.test.ts +139 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +35 -3
  22. package/src/__tests__/config-schema.test.ts +1 -0
  23. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
  24. package/src/__tests__/conversation-agent-loop.test.ts +180 -7
  25. package/src/__tests__/conversation-error.test.ts +211 -4
  26. package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
  27. package/src/__tests__/conversation-queue.test.ts +126 -0
  28. package/src/__tests__/conversation-retry-route.test.ts +46 -7
  29. package/src/__tests__/conversation-slash.test.ts +4 -2
  30. package/src/__tests__/conversation-summarize-route.test.ts +7 -1
  31. package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
  32. package/src/__tests__/document-create-dedupe.test.ts +2 -1
  33. package/src/__tests__/document-find-replace.test.ts +2 -1
  34. package/src/__tests__/document-tool-security.test.ts +2 -1
  35. package/src/__tests__/document-update-default-surface.test.ts +2 -1
  36. package/src/__tests__/document-workspace-file.test.ts +467 -0
  37. package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
  38. package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
  39. package/src/__tests__/http-user-message-parity.test.ts +66 -0
  40. package/src/__tests__/list-messages-attachments.test.ts +87 -0
  41. package/src/__tests__/list-messages-provider-error.test.ts +143 -0
  42. package/src/__tests__/list-messages-system-card.test.ts +100 -0
  43. package/src/__tests__/llm-resolver.test.ts +19 -9
  44. package/src/__tests__/managed-profile-guard.test.ts +23 -0
  45. package/src/__tests__/notification-platform-adapter.test.ts +130 -2
  46. package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
  47. package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
  48. package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
  49. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  50. package/src/__tests__/provider-error-scenarios.test.ts +140 -0
  51. package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
  52. package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
  53. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
  54. package/src/__tests__/skills.test.ts +27 -0
  55. package/src/__tests__/slack-channels-routes.test.ts +0 -2
  56. package/src/__tests__/slack-share-routes.test.ts +0 -3
  57. package/src/__tests__/slack-users-routes.test.ts +0 -2
  58. package/src/__tests__/subagent-tools.test.ts +11 -0
  59. package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
  60. package/src/__tests__/tool-result-spool.test.ts +5 -4
  61. package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
  62. package/src/__tests__/turn-events-store.test.ts +26 -0
  63. package/src/__tests__/ui-visual-surface.test.ts +695 -0
  64. package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
  65. package/src/__tests__/visible-app-context.test.ts +189 -0
  66. package/src/__tests__/workspace-git-service.test.ts +173 -33
  67. package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
  68. package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
  69. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
  70. package/src/agent/loop.ts +36 -7
  71. package/src/api/events/context-window-usage.ts +31 -0
  72. package/src/api/events/notification-intent.ts +8 -0
  73. package/src/api/events/ui-surface-pending.ts +35 -0
  74. package/src/api/index.ts +14 -0
  75. package/src/api/responses/conversation-message.ts +25 -4
  76. package/src/api/surfaces.ts +90 -1
  77. package/src/approvals/guardian-card-withdrawal.ts +66 -31
  78. package/src/approvals/guardian-decision-primitive.ts +4 -0
  79. package/src/calls/__tests__/call-setup-router.test.ts +156 -26
  80. package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
  81. package/src/calls/call-setup-router.ts +80 -36
  82. package/src/calls/voice-session-bridge.ts +173 -26
  83. package/src/cli/commands/inference.help.ts +3 -3
  84. package/src/cli/commands/notifications.help.ts +3 -2
  85. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
  86. package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
  87. package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
  88. package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
  89. package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
  90. package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
  91. package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
  92. package/src/cli/commands/platform/index.help.ts +92 -0
  93. package/src/cli/commands/platform/index.ts +7 -0
  94. package/src/cli/commands/platform/invoices.ts +132 -0
  95. package/src/cli/commands/usage.help.ts +1 -1
  96. package/src/cli/lib/list-installed-plugins.ts +2 -1
  97. package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
  98. package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
  99. package/src/config/assistant-feature-flags.ts +7 -2
  100. package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
  101. package/src/config/bundled-skills/subagent/SKILL.md +3 -1
  102. package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
  103. package/src/config/bundled-skills/visualize/SKILL.md +163 -0
  104. package/src/config/call-site-defaults.ts +3 -2
  105. package/src/config/default-profile-catalog.ts +75 -70
  106. package/src/config/default-profile-names.ts +13 -28
  107. package/src/config/env-registry.ts +1 -0
  108. package/src/config/feature-flag-registry.json +16 -0
  109. package/src/config/llm-resolver.ts +21 -5
  110. package/src/config/loader.ts +18 -11
  111. package/src/config/schemas/llm.ts +1 -9
  112. package/src/config/schemas/memory-retrospective.ts +9 -0
  113. package/src/config/schemas/monitoring.ts +28 -2
  114. package/src/config/schemas/workspace-git.ts +15 -0
  115. package/src/config/seed-inference-profiles.ts +19 -5
  116. package/src/context/post-turn-tool-result-truncation.ts +2 -2
  117. package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
  118. package/src/conversations/message-consolidation.ts +17 -15
  119. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
  120. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
  121. package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
  122. package/src/daemon/conversation-agent-loop.ts +107 -22
  123. package/src/daemon/conversation-error.ts +169 -48
  124. package/src/daemon/conversation-messaging.ts +87 -4
  125. package/src/daemon/conversation-process.ts +57 -45
  126. package/src/daemon/conversation-runtime-assembly.ts +57 -0
  127. package/src/daemon/conversation-store.ts +36 -1
  128. package/src/daemon/conversation-surfaces.ts +68 -6
  129. package/src/daemon/conversation-turn-finalize.ts +76 -18
  130. package/src/daemon/conversation.ts +122 -27
  131. package/src/daemon/lifecycle.ts +9 -0
  132. package/src/daemon/message-types/conversations.ts +8 -0
  133. package/src/daemon/message-types/surfaces.ts +3 -0
  134. package/src/documents/document-store.ts +247 -10
  135. package/src/home/__tests__/feed-types.test.ts +43 -8
  136. package/src/home/__tests__/feed-writer.test.ts +59 -0
  137. package/src/home/feed-types.ts +34 -7
  138. package/src/home/feed-writer.ts +15 -6
  139. package/src/live-voice/__tests__/activity-label.test.ts +95 -0
  140. package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
  141. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
  142. package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
  143. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
  144. package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
  145. package/src/live-voice/activity-label.ts +169 -0
  146. package/src/live-voice/live-activity-reporter.ts +35 -6
  147. package/src/live-voice/live-voice-session.ts +282 -41
  148. package/src/live-voice/protocol.ts +35 -0
  149. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
  150. package/src/messaging/providers/slack/adapter.ts +86 -19
  151. package/src/messaging/providers/slack/api.test.ts +85 -1
  152. package/src/messaging/providers/slack/api.ts +121 -288
  153. package/src/messaging/providers/slack/client.ts +20 -251
  154. package/src/messaging/providers/slack/send.test.ts +4 -9
  155. package/src/messaging/providers/slack/send.ts +1 -1
  156. package/src/messaging/providers/slack/types.ts +5 -0
  157. package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
  158. package/src/messaging/providers/slack/web-api-transport.ts +367 -0
  159. package/src/messaging/providers/slack/withdraw.ts +6 -14
  160. package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
  161. package/src/messaging/providers/telegram-bot/send.ts +23 -2
  162. package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
  163. package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
  164. package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
  165. package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
  166. package/src/monitoring/file-descriptors.ts +262 -0
  167. package/src/monitoring/process-memory.ts +4 -11
  168. package/src/monitoring/resource-sampler.ts +5 -0
  169. package/src/monitoring/worker.ts +12 -0
  170. package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
  171. package/src/notifications/__tests__/broadcaster.test.ts +347 -7
  172. package/src/notifications/__tests__/copy-composer.test.ts +53 -3
  173. package/src/notifications/__tests__/decision-engine.test.ts +349 -29
  174. package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
  175. package/src/notifications/__tests__/edit-notification.test.ts +330 -0
  176. package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
  177. package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
  178. package/src/notifications/adapters/macos.ts +3 -0
  179. package/src/notifications/adapters/platform.ts +90 -14
  180. package/src/notifications/adapters/telegram.ts +6 -4
  181. package/src/notifications/assistant-reply-producer.ts +219 -0
  182. package/src/notifications/broadcaster.ts +523 -317
  183. package/src/notifications/copy-composer.ts +27 -6
  184. package/src/notifications/decision-engine.ts +154 -77
  185. package/src/notifications/deterministic-checks.ts +6 -7
  186. package/src/notifications/edit-notification.ts +8 -4
  187. package/src/notifications/emit-signal.ts +49 -16
  188. package/src/notifications/guardian-delivery-recorder.ts +8 -2
  189. package/src/notifications/home-feed-side-effect.ts +43 -6
  190. package/src/notifications/notification-utils.ts +39 -4
  191. package/src/notifications/signal.ts +10 -0
  192. package/src/notifications/types.ts +17 -0
  193. package/src/persistence/bookmark-crud.ts +18 -9
  194. package/src/persistence/conversation-attention-store.ts +26 -0
  195. package/src/persistence/conversation-crud.ts +79 -32
  196. package/src/persistence/conversation-queries.ts +30 -15
  197. package/src/persistence/conversation-title-service.ts +5 -170
  198. package/src/persistence/conversation-types.ts +192 -1
  199. package/src/persistence/db-init.ts +14 -1
  200. package/src/persistence/db-maintenance.ts +5 -4
  201. package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
  202. package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
  203. package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
  204. package/src/persistence/embeddings/embedding-local.ts +2 -1
  205. package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
  206. package/src/persistence/embeddings/plugin-index.ts +84 -12
  207. package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
  208. package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
  209. package/src/persistence/planner-statistics.ts +14 -0
  210. package/src/persistence/schema/documents.ts +26 -11
  211. package/src/persistence/steps.ts +2 -0
  212. package/src/platform/client.test.ts +172 -2
  213. package/src/platform/client.ts +178 -55
  214. package/src/plugin-api/conversation-turn.ts +13 -0
  215. package/src/plugin-api/model-profiles.test.ts +6 -2
  216. package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
  217. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
  218. package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
  219. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
  220. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
  224. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
  225. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
  226. package/src/plugins/defaults/memory/host-utils.ts +5 -0
  227. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
  228. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
  229. package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
  230. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
  231. package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
  232. package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
  233. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
  234. package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
  235. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
  236. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
  237. package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
  238. package/src/plugins/defaults/memory/v2/injection.ts +48 -3
  239. package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
  240. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
  241. package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
  242. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  243. package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
  244. package/src/plugins/types.ts +17 -0
  245. package/src/prompts/templates/system-sections.ts +2 -2
  246. package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
  247. package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
  248. package/src/providers/call-site-routing.ts +77 -14
  249. package/src/providers/connection-resolution.ts +42 -2
  250. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
  251. package/src/providers/inference/adapter-factory.ts +70 -12
  252. package/src/providers/model-catalog.ts +0 -11
  253. package/src/providers/model-intents.ts +11 -0
  254. package/src/providers/registry.ts +8 -0
  255. package/src/providers/retry.ts +85 -17
  256. package/src/providers/types.ts +8 -1
  257. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  258. package/src/runtime/agent-wake.ts +132 -9
  259. package/src/runtime/channel-approval-types.ts +25 -0
  260. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
  261. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
  262. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
  263. package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
  264. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
  265. package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
  266. package/src/runtime/routes/consolidation-routes.ts +1 -1
  267. package/src/runtime/routes/conversation-management-routes.ts +18 -6
  268. package/src/runtime/routes/conversation-query-routes.ts +14 -23
  269. package/src/runtime/routes/conversation-routes.ts +113 -8
  270. package/src/runtime/routes/credential-routes.ts +1 -5
  271. package/src/runtime/routes/documents-routes.ts +207 -12
  272. package/src/runtime/routes/identity-routes.ts +3 -93
  273. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
  274. package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
  275. package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
  276. package/src/runtime/routes/notification-routes.ts +4 -1
  277. package/src/runtime/routes/platform-routes.ts +238 -3
  278. package/src/runtime/routes/playground/guard.ts +1 -2
  279. package/src/runtime/routes/slack-channel-routes.ts +2 -4
  280. package/src/runtime/routes/workspace-commit-routes.ts +49 -2
  281. package/src/runtime/routes/workspace-routes.ts +11 -12
  282. package/src/runtime/routes/workspace-utils.ts +49 -2
  283. package/src/runtime/services/conversation-serializer.ts +2 -4
  284. package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
  285. package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
  286. package/src/subagent/__tests__/consult-context.test.ts +84 -0
  287. package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
  288. package/src/subagent/consult-context.ts +410 -0
  289. package/src/subagent/consult-prompt.ts +38 -5
  290. package/src/subagent/manager.ts +9 -4
  291. package/src/subagent/types.ts +8 -0
  292. package/src/telemetry/telemetry-event-sources.ts +7 -0
  293. package/src/telemetry/telemetry-wire-source.json +1 -1
  294. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  295. package/src/telemetry/turn-events-store.ts +30 -1
  296. package/src/telemetry/types.ts +24 -0
  297. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  298. package/src/tools/browser/pinned-tabs.ts +3 -1
  299. package/src/tools/subagent/spawn.ts +23 -1
  300. package/src/tools/terminal/safe-env.ts +1 -0
  301. package/src/tools/ui-surface/definitions.ts +39 -1
  302. package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
  303. package/src/tools/ui-surface/visual-validation.ts +787 -0
  304. package/src/tools/workflows/run-workflow.test.ts +1 -0
  305. package/src/util/__tests__/short-title.test.ts +229 -0
  306. package/src/util/__tests__/worker-compute.test.ts +65 -0
  307. package/src/util/cgroup-cpu.ts +93 -0
  308. package/src/util/errors.ts +27 -0
  309. package/src/util/process-tree.ts +19 -0
  310. package/src/util/short-title.ts +189 -0
  311. package/src/util/worker-compute.ts +83 -0
  312. package/src/workspace/byok-default-profile-ensure.ts +25 -15
  313. package/src/workspace/git-service.ts +79 -30
  314. package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
  315. package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
  316. package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
  317. package/src/workspace/migrations/registry.ts +6 -0
  318. package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
@@ -1,20 +1,23 @@
1
1
  /**
2
- * Tests for the assistant_tool pass-through path in the notification
3
- * decision engine. When a producer hands us a verbatim message via
4
- * contextPayload.requestedMessage, the engine must skip the LLM call
5
- * entirely and use the producer-supplied copy as-is.
2
+ * Tests for the pass-through paths in the notification decision engine. When a
3
+ * producer hands us a verbatim message via contextPayload.requestedMessage, the
4
+ * engine must skip the LLM call entirely and use the copy as-is.
6
5
  */
7
6
 
8
- import { describe, expect, mock, test } from "bun:test";
7
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
9
8
 
10
9
  // ── Mocks (must precede imports from mocked modules) ──────────────────
11
10
 
12
11
  mock.module("../../channels/config.js", () => ({
13
- getDeliverableChannels: () => ["vellum", "telegram"],
12
+ getDeliverableChannels: () => ["vellum", "telegram", "platform"],
14
13
  }));
15
14
 
15
+ let persistedDecisions: Array<Record<string, unknown>> = [];
16
+
16
17
  mock.module("../decisions-store.js", () => ({
17
- createDecision: () => {},
18
+ createDecision: (row: Record<string, unknown>) => {
19
+ persistedDecisions.push(row);
20
+ },
18
21
  }));
19
22
 
20
23
  mock.module("../preference-summary.js", () => ({
@@ -49,28 +52,34 @@ mock.module("../../contacts/contact-store.js", () => ({
49
52
  contactInfoFixture[contactId] ?? null,
50
53
  }));
51
54
 
52
- // Provider mock. By default `sendMessage` throws so the assistant_tool
53
- // pass-through path (which must skip the LLM) fails loudly if it reaches the
54
- // provider. LLM-path tests override `providerSendMessage` to capture inputs.
55
+ // Provider mock. By default `sendMessage` throws so the pass-through paths
56
+ // (which must skip the LLM) fail loudly if they reach the provider. LLM-path
57
+ // tests override `providerSendMessage` to capture inputs.
58
+ type ProviderSendOptions = { systemPrompt?: string; tools?: unknown[] };
59
+
55
60
  let providerSendMessage: (
56
61
  messages: unknown[],
57
- opts: { systemPrompt?: string },
62
+ opts: ProviderSendOptions,
58
63
  ) => Promise<unknown> = () => {
59
64
  throw new Error(
60
- "provider.sendMessage should NOT be invoked for assistant_tool pass-through",
65
+ "provider.sendMessage should NOT be invoked for pass-through decisions",
61
66
  );
62
67
  };
63
68
 
69
+ // Tool block the engine reads back from a provider response. Null drives the
70
+ // deterministic fallback, so LLM-path tests set it before calling in.
71
+ let toolUseBlock: { input: Record<string, unknown> } | null = null;
72
+
64
73
  mock.module("../../providers/provider-send-message.js", () => ({
65
74
  getConfiguredProvider: async () => ({
66
- sendMessage: (messages: unknown[], opts: { systemPrompt?: string }) =>
75
+ sendMessage: (messages: unknown[], opts: ProviderSendOptions) =>
67
76
  providerSendMessage(messages, opts),
68
77
  }),
69
78
  createTimeout: () => ({
70
79
  signal: new AbortController().signal,
71
80
  cleanup: () => {},
72
81
  }),
73
- extractToolUse: () => null,
82
+ extractToolUse: () => toolUseBlock,
74
83
  userMessage: (text: string) => ({ role: "user", content: text }),
75
84
  }));
76
85
 
@@ -105,6 +114,47 @@ function makeAssistantToolSignal(
105
114
  };
106
115
  }
107
116
 
117
+ function makeAssistantReplySignal(
118
+ overrides?: Partial<NotificationSignal>,
119
+ ): NotificationSignal {
120
+ return {
121
+ signalId: "sig-assistant-reply-test-1",
122
+ createdAt: Date.now(),
123
+ sourceChannel: "vellum",
124
+ sourceContextId: "conv-1",
125
+ sourceEventName: "chat.assistant_reply",
126
+ contextPayload: {
127
+ requestedMessage: "Here is your answer.",
128
+ requestedTitle: "Assistant",
129
+ },
130
+ attentionHints: {
131
+ requiresAction: false,
132
+ urgency: "medium",
133
+ isAsyncBackground: false,
134
+ visibleInSourceNow: false,
135
+ },
136
+ ...overrides,
137
+ };
138
+ }
139
+
140
+ /** A signal with no verbatim copy, so the engine takes the LLM path. */
141
+ function makeLlmSignal(): NotificationSignal {
142
+ return {
143
+ signalId: "sig-llm-1",
144
+ createdAt: Date.now(),
145
+ sourceChannel: "scheduler",
146
+ sourceContextId: "schedule-1",
147
+ sourceEventName: "schedule.notify",
148
+ contextPayload: {},
149
+ attentionHints: {
150
+ requiresAction: false,
151
+ urgency: "low",
152
+ isAsyncBackground: false,
153
+ visibleInSourceNow: false,
154
+ },
155
+ };
156
+ }
157
+
108
158
  // ── Tests ─────────────────────────────────────────────────────────────
109
159
 
110
160
  describe("assistant_tool pass-through in notification decision engine", () => {
@@ -121,11 +171,25 @@ describe("assistant_tool pass-through in notification decision engine", () => {
121
171
  expect(decision.renderedCopy.vellum?.title).toBe("Custom Title");
122
172
  expect(decision.conversationActions?.vellum?.action).toBe("start_new");
123
173
  expect(decision.reasoningSummary).toBe("assistant_tool pass-through");
174
+ expect(decision.verbatimCopy).toBe(true);
124
175
  expect(decision.fallbackUsed).toBe(false);
125
176
  expect(decision.confidence).toBe(1.0);
126
177
  expect(decision.dedupeKey).toBe(signal.signalId);
127
178
  });
128
179
 
180
+ // `notify --dedupe-key` reaches the signal, so the decision has to carry it
181
+ // through; falling back to the per-emit signal id would collapse nothing.
182
+ test("uses the producer-supplied dedupeKey", async () => {
183
+ const signal = makeAssistantToolSignal({
184
+ dedupeKey: "deploy-status:prod",
185
+ });
186
+ const decision = await evaluateSignal(signal, [
187
+ "vellum",
188
+ ] as NotificationChannel[]);
189
+
190
+ expect(decision.dedupeKey).toBe("deploy-status:prod");
191
+ });
192
+
129
193
  test("derives title from body when requestedTitle is not supplied", async () => {
130
194
  const signal = makeAssistantToolSignal({
131
195
  contextPayload: {
@@ -294,24 +358,157 @@ describe("assistant_tool pass-through in notification decision engine", () => {
294
358
  });
295
359
  });
296
360
 
297
- describe("recipient notes injection (ACL from gateway, notes joined locally)", () => {
298
- function makeLlmSignal(): NotificationSignal {
299
- return {
300
- signalId: "sig-llm-notes-1",
301
- createdAt: Date.now(),
302
- sourceChannel: "scheduler",
303
- sourceContextId: "schedule-1",
304
- sourceEventName: "schedule.notify",
305
- contextPayload: {},
306
- attentionHints: {
307
- requiresAction: false,
308
- urgency: "low",
309
- isAsyncBackground: false,
310
- visibleInSourceNow: false,
361
+ describe("chat.assistant_reply pass-through in notification decision engine", () => {
362
+ beforeEach(() => {
363
+ persistedDecisions = [];
364
+ });
365
+
366
+ test("uses producer-supplied title and body verbatim, no LLM call", async () => {
367
+ const signal = makeAssistantReplySignal();
368
+ const decision = await evaluateSignal(signal, [
369
+ "vellum",
370
+ "platform",
371
+ ] as NotificationChannel[]);
372
+
373
+ expect(decision.shouldNotify).toBe(true);
374
+ expect(decision.renderedCopy.platform?.body).toBe("Here is your answer.");
375
+ expect(decision.renderedCopy.platform?.title).toBe("Assistant");
376
+ expect(decision.reasoningSummary).toBe("assistant_reply pass-through");
377
+ expect(decision.verbatimCopy).toBe(true);
378
+ expect(decision.fallbackUsed).toBe(false);
379
+ expect(decision.confidence).toBe(1.0);
380
+ });
381
+
382
+ test("selects exactly the platform channel when platform is available", async () => {
383
+ const decision = await evaluateSignal(makeAssistantReplySignal(), [
384
+ "vellum",
385
+ "telegram",
386
+ "platform",
387
+ ] as NotificationChannel[]);
388
+
389
+ expect(decision.selectedChannels).toEqual(["platform"]);
390
+ expect(decision.shouldNotify).toBe(true);
391
+ });
392
+
393
+ test("selects nothing and suppresses when platform is unavailable", async () => {
394
+ const decision = await evaluateSignal(makeAssistantReplySignal(), [
395
+ "vellum",
396
+ "telegram",
397
+ ] as NotificationChannel[]);
398
+
399
+ expect(decision.selectedChannels).toEqual([]);
400
+ expect(decision.shouldNotify).toBe(false);
401
+ });
402
+
403
+ test("seeds rendered copy for every available channel, not just the selected one", async () => {
404
+ // A future channel added to ASSISTANT_REPLY_CHANNELS (or appended by a
405
+ // downstream guard) inherits the verbatim copy instead of falling back.
406
+ const available = [
407
+ "vellum",
408
+ "telegram",
409
+ "platform",
410
+ ] as NotificationChannel[];
411
+ const decision = await evaluateSignal(
412
+ makeAssistantReplySignal(),
413
+ available,
414
+ );
415
+
416
+ for (const ch of available) {
417
+ expect(decision.renderedCopy[ch]?.body).toBe("Here is your answer.");
418
+ expect(decision.renderedCopy[ch]?.title).toBe("Assistant");
419
+ expect(decision.conversationActions?.[ch]?.action).toBe("start_new");
420
+ }
421
+ });
422
+
423
+ test("derives the title from the body when requestedTitle is not supplied", async () => {
424
+ const signal = makeAssistantReplySignal({
425
+ contextPayload: {
426
+ requestedMessage: "First sentence. Second sentence follows here.",
311
427
  },
428
+ });
429
+ const decision = await evaluateSignal(signal, [
430
+ "platform",
431
+ ] as NotificationChannel[]);
432
+
433
+ expect(decision.renderedCopy.platform?.title).toBe("First sentence.");
434
+ });
435
+
436
+ test("uses the producer-supplied dedupeKey", async () => {
437
+ const signal = makeAssistantReplySignal({
438
+ dedupeKey: "chat.assistant_reply:conv-1:msg-1",
439
+ });
440
+ const decision = await evaluateSignal(signal, [
441
+ "platform",
442
+ ] as NotificationChannel[]);
443
+
444
+ expect(decision.dedupeKey).toBe("chat.assistant_reply:conv-1:msg-1");
445
+ });
446
+
447
+ test("falls back to the signal id when the producer supplies no dedupeKey", async () => {
448
+ const signal = makeAssistantReplySignal();
449
+ const decision = await evaluateSignal(signal, [
450
+ "platform",
451
+ ] as NotificationChannel[]);
452
+
453
+ expect(decision.dedupeKey).toBe(signal.signalId);
454
+ });
455
+
456
+ test("threads contextPayload.deepLinkMetadata through to decision.deepLinkTarget", async () => {
457
+ const signal = makeAssistantReplySignal({
458
+ contextPayload: {
459
+ requestedMessage: "Here is your answer.",
460
+ deepLinkMetadata: { conversationId: "conv-1" },
461
+ },
462
+ });
463
+ const decision = await evaluateSignal(signal, [
464
+ "platform",
465
+ ] as NotificationChannel[]);
466
+
467
+ expect(decision.deepLinkTarget).toEqual({ conversationId: "conv-1" });
468
+ });
469
+
470
+ test("persists the decision", async () => {
471
+ const signal = makeAssistantReplySignal();
472
+ const decision = await evaluateSignal(signal, [
473
+ "platform",
474
+ ] as NotificationChannel[]);
475
+
476
+ expect(decision.persistedDecisionId).toBeDefined();
477
+ expect(persistedDecisions.length).toBe(1);
478
+ expect(persistedDecisions[0]?.notificationEventId).toBe(signal.signalId);
479
+ expect(persistedDecisions[0]?.reasoningSummary).toBe(
480
+ "assistant_reply pass-through",
481
+ );
482
+ });
483
+
484
+ test("an empty body skips the pass-through branch entirely", async () => {
485
+ const signal = makeAssistantReplySignal({
486
+ contextPayload: { requestedMessage: " ", requestedTitle: "Assistant" },
487
+ });
488
+ const previousSendMessage = providerSendMessage;
489
+ let providerCalled = false;
490
+ providerSendMessage = async () => {
491
+ providerCalled = true;
492
+ return {};
312
493
  };
313
- }
314
494
 
495
+ try {
496
+ const decision = await evaluateSignal(signal, [
497
+ "platform",
498
+ ] as NotificationChannel[]);
499
+
500
+ expect(providerCalled).toBe(true);
501
+ expect(decision.verbatimCopy).toBeUndefined();
502
+ expect(decision.reasoningSummary).not.toBe(
503
+ "assistant_reply pass-through",
504
+ );
505
+ } finally {
506
+ providerSendMessage = previousSendMessage;
507
+ }
508
+ });
509
+ });
510
+
511
+ describe("recipient notes injection (ACL from gateway, notes joined locally)", () => {
315
512
  test("injects the guardian's local notes, resolved via the gateway contactId", async () => {
316
513
  guardianDeliveryFixture = [{ contactId: "contact-42" }];
317
514
  contactInfoFixture = { "contact-42": { notes: "Prefers terse updates." } };
@@ -343,3 +540,126 @@ describe("recipient notes injection (ACL from gateway, notes joined locally)", (
343
540
  expect(capturedSystemPrompt).not.toContain("<recipient-context>");
344
541
  });
345
542
  });
543
+
544
+ describe("decision tool title field specification", () => {
545
+ test("pins the length, form, and no-echo constraints in the tool schema", async () => {
546
+ guardianDeliveryFixture = [];
547
+ contactInfoFixture = {};
548
+
549
+ let capturedTools: unknown[] | undefined;
550
+ providerSendMessage = async (_messages, opts) => {
551
+ capturedTools = opts.tools;
552
+ return {};
553
+ };
554
+
555
+ await evaluateSignal(makeLlmSignal(), ["vellum"] as NotificationChannel[]);
556
+
557
+ const tool = capturedTools?.[0] as {
558
+ input_schema: {
559
+ properties: {
560
+ renderedCopy: {
561
+ properties: Record<
562
+ string,
563
+ { properties: { title: { description: string } } }
564
+ >;
565
+ };
566
+ };
567
+ };
568
+ };
569
+ const description =
570
+ tool.input_schema.properties.renderedCopy.properties.vellum.properties
571
+ .title.description;
572
+
573
+ expect(description).toContain("2 to 5 words");
574
+ expect(description).toContain("40 characters");
575
+ expect(description).toContain("noun phrase");
576
+ expect(description).toContain(
577
+ "Do NOT restate, summarize, or echo the body",
578
+ );
579
+ expect(description).toContain("no markdown");
580
+ expect(description).toContain("Missing Context");
581
+ expect(description.match(/NOT '/g)?.length ?? 0).toBeGreaterThanOrEqual(2);
582
+ });
583
+ });
584
+
585
+ describe("title normalization", () => {
586
+ /** Run the LLM path with the given channel copy and return the kept title. */
587
+ async function titleFromModelCopy(
588
+ title: string,
589
+ body: string,
590
+ ): Promise<string | undefined> {
591
+ guardianDeliveryFixture = [];
592
+ contactInfoFixture = {};
593
+ const previousSendMessage = providerSendMessage;
594
+ providerSendMessage = async () => ({});
595
+ toolUseBlock = {
596
+ input: {
597
+ shouldNotify: true,
598
+ selectedChannels: ["vellum"],
599
+ reasoningSummary: "model decision",
600
+ dedupeKey: "title-normalization-test",
601
+ renderedCopy: { vellum: { title, body } },
602
+ },
603
+ };
604
+
605
+ try {
606
+ const decision = await evaluateSignal(makeLlmSignal(), [
607
+ "vellum",
608
+ ] as NotificationChannel[]);
609
+ return decision.renderedCopy.vellum?.title;
610
+ } finally {
611
+ toolUseBlock = null;
612
+ providerSendMessage = previousSendMessage;
613
+ }
614
+ }
615
+
616
+ test("replaces a model title that restates the body with a derived one", async () => {
617
+ const title = "The staging deploy finished and the build is live.";
618
+ const body = `Deploy complete. ${title}`;
619
+
620
+ expect(await titleFromModelCopy(title, body)).toBe("Deploy complete.");
621
+ });
622
+
623
+ test("keeps a clean model title verbatim", async () => {
624
+ expect(
625
+ await titleFromModelCopy(
626
+ "Staging Deploy Status",
627
+ "The staging deploy finished.",
628
+ ),
629
+ ).toBe("Staging Deploy Status");
630
+ });
631
+
632
+ test("truncates a model title longer than 40 characters", async () => {
633
+ const kept = await titleFromModelCopy(
634
+ "Quarterly Infrastructure Migration Status Report",
635
+ "The migration is on track for the quarter.",
636
+ );
637
+
638
+ expect(kept).toBe("Quarterly Infrastructure Migration");
639
+ expect(kept?.length).toBeLessThanOrEqual(40);
640
+ });
641
+
642
+ test("strips markdown from a model title", async () => {
643
+ expect(
644
+ await titleFromModelCopy(
645
+ "**Deploy Status**",
646
+ "The staging deploy finished.",
647
+ ),
648
+ ).toBe("Deploy Status");
649
+ });
650
+
651
+ test("replaces a pass-through requestedTitle that stutters against the body", async () => {
652
+ const requestedTitle = "The nightly backup finished without any errors.";
653
+ const signal = makeAssistantToolSignal({
654
+ contextPayload: {
655
+ requestedMessage: `Backup finished. ${requestedTitle}`,
656
+ requestedTitle,
657
+ },
658
+ });
659
+ const decision = await evaluateSignal(signal, [
660
+ "vellum",
661
+ ] as NotificationChannel[]);
662
+
663
+ expect(decision.renderedCopy.vellum?.title).toBe("Backup finished.");
664
+ });
665
+ });
@@ -241,6 +241,7 @@ describe("checkRenderedCopyQuality (via runDeterministicChecks)", () => {
241
241
  });
242
242
  const decision = makeDecision({
243
243
  reasoningSummary: "assistant_tool pass-through",
244
+ verbatimCopy: true,
244
245
  renderedCopy: {
245
246
  vellum: { title: "Assistant share", body: "assistant share" },
246
247
  },
@@ -253,6 +254,7 @@ describe("checkRenderedCopyQuality (via runDeterministicChecks)", () => {
253
254
  const signal = makeSignal({ sourceChannel: "assistant_tool" });
254
255
  const decision = makeDecision({
255
256
  reasoningSummary: "assistant_tool pass-through",
257
+ verbatimCopy: true,
256
258
  renderedCopy: {
257
259
  vellum: { title: "Reminder", body: "" },
258
260
  },
@@ -262,6 +264,25 @@ describe("checkRenderedCopyQuality (via runDeterministicChecks)", () => {
262
264
  expect(result.reason).toContain("empty");
263
265
  });
264
266
 
267
+ test("passes assistant_reply pass-through after downstream suffixes mutate the reasoning summary", async () => {
268
+ // `emit-signal` and routing-intent enforcement append suffixes to
269
+ // `reasoningSummary`, so the exemption must key off `verbatimCopy`.
270
+ const signal = makeSignal({
271
+ sourceChannel: "vellum",
272
+ sourceEventName: "chat.assistant_reply",
273
+ });
274
+ const decision = makeDecision({
275
+ reasoningSummary:
276
+ "assistant_reply pass-through (vellum forced: high urgency)",
277
+ verbatimCopy: true,
278
+ renderedCopy: {
279
+ vellum: { title: "Assistant", body: "chat assistant reply" },
280
+ },
281
+ });
282
+ const result = await runDeterministicChecks(signal, decision, context);
283
+ expect(result.passed).toBe(true);
284
+ });
285
+
265
286
  test("still fails non-pass-through decision when body matches event name", async () => {
266
287
  // Regression guard: the pass-through short-circuit must not weaken
267
288
  // the check for LLM/fallback paths.