@vellumai/assistant 0.8.2 → 0.8.4

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 (503) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -2
  3. package/docker-init-apt-root.sh +79 -6
  4. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  5. package/openapi.yaml +945 -36
  6. package/package.json +1 -1
  7. package/src/__tests__/agent-loop-exit-reason.test.ts +271 -0
  8. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  9. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  10. package/src/__tests__/agent-loop.test.ts +88 -3
  11. package/src/__tests__/anthropic-provider.test.ts +272 -0
  12. package/src/__tests__/approval-cascade.test.ts +1 -1
  13. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  14. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  15. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  16. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  17. package/src/__tests__/compaction-events.test.ts +1 -1
  18. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  19. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  20. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  21. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  22. package/src/__tests__/config-watcher.test.ts +1 -1
  23. package/src/__tests__/context-token-estimator.test.ts +112 -57
  24. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  25. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  26. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  27. package/src/__tests__/conversation-agent-loop.test.ts +77 -3
  28. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  29. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  30. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  31. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  32. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  33. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  34. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  35. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  36. package/src/__tests__/conversation-pairing.test.ts +2 -2
  37. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  38. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  39. package/src/__tests__/conversation-queue.test.ts +1 -1
  40. package/src/__tests__/conversation-runtime-assembly.test.ts +290 -85
  41. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  42. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  43. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  44. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  45. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  46. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  47. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  48. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  49. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  50. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  51. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  52. package/src/__tests__/date-context.test.ts +45 -0
  53. package/src/__tests__/dm-backfill.test.ts +64 -0
  54. package/src/__tests__/dm-persistence.test.ts +33 -0
  55. package/src/__tests__/document-find-replace.test.ts +501 -0
  56. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  57. package/src/__tests__/first-greeting.test.ts +23 -2
  58. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  59. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  60. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  61. package/src/__tests__/heartbeat-service.test.ts +24 -164
  62. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  63. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  64. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  65. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  66. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  67. package/src/__tests__/host-file-proxy.test.ts +8 -1
  68. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  69. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  70. package/src/__tests__/identity-routes.test.ts +57 -0
  71. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  72. package/src/__tests__/injector-background-turn.test.ts +153 -0
  73. package/src/__tests__/injector-chain.test.ts +7 -0
  74. package/src/__tests__/injector-document-comments.test.ts +378 -0
  75. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  76. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  77. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  78. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  79. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  80. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  81. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  82. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  83. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  84. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  85. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  86. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  87. package/src/__tests__/llm-resolver.test.ts +340 -3
  88. package/src/__tests__/log-export-routes.test.ts +99 -2
  89. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  90. package/src/__tests__/message-queue-steer.test.ts +114 -0
  91. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  92. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  93. package/src/__tests__/notification-deep-link.test.ts +15 -0
  94. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  95. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  96. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  97. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  98. package/src/__tests__/openai-provider.test.ts +323 -3
  99. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  100. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  101. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  102. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  103. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  104. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  105. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  106. package/src/__tests__/platform.test.ts +0 -3
  107. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  108. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  109. package/src/__tests__/plugin-types.test.ts +2 -2
  110. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  111. package/src/__tests__/process-message-display-content.test.ts +21 -16
  112. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  113. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  114. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  115. package/src/__tests__/server-history-render.test.ts +83 -4
  116. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  117. package/src/__tests__/system-prompt.test.ts +57 -101
  118. package/src/__tests__/terminal-tools.test.ts +11 -1
  119. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  120. package/src/__tests__/thread-backfill.test.ts +370 -22
  121. package/src/__tests__/tool-executor.test.ts +90 -1
  122. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  123. package/src/__tests__/twilio-routes.test.ts +1 -1
  124. package/src/__tests__/web-fetch.test.ts +2 -2
  125. package/src/__tests__/workspace-git-service.test.ts +88 -5
  126. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  127. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  128. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  129. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  130. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  131. package/src/a2a/__tests__/task-store.test.ts +246 -0
  132. package/src/a2a/agent-card.ts +58 -0
  133. package/src/a2a/feature-gate.ts +8 -0
  134. package/src/a2a/protocol-constants.ts +21 -0
  135. package/src/a2a/protocol-errors.ts +50 -0
  136. package/src/a2a/protocol-types.ts +162 -0
  137. package/src/a2a/task-store.ts +168 -0
  138. package/src/agent/attachments.ts +1 -0
  139. package/src/agent/loop.ts +208 -22
  140. package/src/background-wake/next-wake.test.ts +289 -0
  141. package/src/background-wake/next-wake.ts +172 -0
  142. package/src/browser/operations.ts +15 -0
  143. package/src/channels/config.ts +9 -0
  144. package/src/channels/types.ts +14 -0
  145. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  146. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  147. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  148. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  149. package/src/cli/commands/conversations.ts +128 -1
  150. package/src/cli/commands/inference-providers.ts +147 -1
  151. package/src/cli/commands/memory-v2.ts +308 -0
  152. package/src/cli/commands/notifications.ts +89 -37
  153. package/src/cli/commands/plugins.ts +67 -0
  154. package/src/cli/commands/schedules.ts +297 -5
  155. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  156. package/src/cli/lib/install-from-github.ts +8 -9
  157. package/src/cli/lib/search-plugins.ts +163 -0
  158. package/src/cli/program.ts +14 -0
  159. package/src/cli/utils/conversation-id.ts +17 -5
  160. package/src/config/assistant-feature-flags.ts +24 -54
  161. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  162. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  163. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  164. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  165. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  166. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  167. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  168. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  169. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  170. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  171. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  172. package/src/config/bundled-tool-registry.ts +22 -12
  173. package/src/config/call-site-defaults.ts +124 -0
  174. package/src/config/feature-flag-registry.json +111 -23
  175. package/src/config/llm-resolver.ts +66 -1
  176. package/src/config/schema.ts +2 -0
  177. package/src/config/schemas/__tests__/memory-v2.test.ts +7 -3
  178. package/src/config/schemas/call-site-catalog.ts +21 -0
  179. package/src/config/schemas/channels.ts +9 -0
  180. package/src/config/schemas/conversations.ts +10 -0
  181. package/src/config/schemas/heartbeat.ts +14 -0
  182. package/src/config/schemas/llm.ts +4 -3
  183. package/src/config/schemas/memory-retrospective.ts +1 -1
  184. package/src/config/schemas/memory-v2.ts +51 -4
  185. package/src/config/schemas/memory.ts +3 -1
  186. package/src/config/seed-inference-profiles.ts +99 -29
  187. package/src/context/compactor.ts +80 -13
  188. package/src/context/token-estimator.ts +72 -31
  189. package/src/context/window-manager.ts +25 -0
  190. package/src/credential-health/credential-health-service.ts +34 -19
  191. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  192. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  193. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  194. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  195. package/src/daemon/conversation-agent-loop-handlers.ts +231 -23
  196. package/src/daemon/conversation-agent-loop.ts +252 -56
  197. package/src/daemon/conversation-lifecycle.ts +142 -116
  198. package/src/daemon/conversation-messaging.ts +3 -0
  199. package/src/daemon/conversation-process.ts +273 -0
  200. package/src/daemon/conversation-queue-manager.ts +14 -0
  201. package/src/daemon/conversation-runtime-assembly.ts +144 -75
  202. package/src/daemon/conversation-slash.ts +37 -5
  203. package/src/daemon/conversation-surfaces.ts +45 -2
  204. package/src/daemon/conversation-tool-setup.ts +7 -0
  205. package/src/daemon/conversation.ts +42 -12
  206. package/src/daemon/date-context.ts +40 -0
  207. package/src/daemon/first-greeting.ts +10 -0
  208. package/src/daemon/guardian-action-generators.ts +1 -125
  209. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  210. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  211. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  212. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  213. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  214. package/src/daemon/handlers/config-a2a.ts +449 -0
  215. package/src/daemon/handlers/config-model.test.ts +1 -0
  216. package/src/daemon/handlers/conversations.ts +80 -0
  217. package/src/daemon/handlers/shared.ts +92 -29
  218. package/src/daemon/host-app-control-proxy.ts +69 -18
  219. package/src/daemon/host-bash-proxy.ts +1 -1
  220. package/src/daemon/host-cu-proxy.ts +1 -1
  221. package/src/daemon/host-file-proxy.ts +1 -1
  222. package/src/daemon/host-proxy-preactivation.ts +85 -18
  223. package/src/daemon/host-transfer-proxy.ts +1 -1
  224. package/src/daemon/lifecycle.ts +67 -65
  225. package/src/daemon/memory-v2-startup.ts +49 -13
  226. package/src/daemon/message-protocol.ts +4 -0
  227. package/src/daemon/message-types/conversations.ts +8 -0
  228. package/src/daemon/message-types/document-comments.ts +50 -0
  229. package/src/daemon/message-types/messages.ts +68 -1
  230. package/src/daemon/message-types/notifications.ts +21 -0
  231. package/src/daemon/message-types/surfaces.ts +3 -1
  232. package/src/daemon/message-types/web-activity.ts +57 -0
  233. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  234. package/src/daemon/pkb-reminder-builder.ts +4 -19
  235. package/src/daemon/plugin-source-watcher.ts +135 -3
  236. package/src/daemon/process-message.ts +72 -12
  237. package/src/daemon/query-complexity-router.ts +75 -0
  238. package/src/daemon/skill-memory-refresh.ts +5 -1
  239. package/src/daemon/trust-context.ts +6 -0
  240. package/src/daemon/wake-target-adapter.ts +2 -0
  241. package/src/documents/document-comments-store.test.ts +338 -0
  242. package/src/documents/document-comments-store.ts +237 -0
  243. package/src/documents/document-store.ts +202 -0
  244. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  245. package/src/export/transcript-formatter.ts +54 -20
  246. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -1
  247. package/src/heartbeat/heartbeat-service.ts +35 -191
  248. package/src/home/__tests__/feed-types.test.ts +40 -0
  249. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  250. package/src/home/feed-types.ts +20 -3
  251. package/src/home/home-content-refresh.ts +52 -0
  252. package/src/home/home-greeting-cache.ts +69 -0
  253. package/src/home/home-greeting.ts +94 -0
  254. package/src/home/suggested-prompts.ts +177 -9
  255. package/src/ipc/cli-client.ts +147 -45
  256. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  257. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  258. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  259. package/src/memory/__tests__/memory-retrospective-job.test.ts +407 -10
  260. package/src/memory/conversation-crud.ts +133 -43
  261. package/src/memory/conversation-queries.ts +87 -1
  262. package/src/memory/conversation-title-service.ts +26 -4
  263. package/src/memory/db-init.ts +22 -0
  264. package/src/memory/delivery-crud.ts +41 -0
  265. package/src/memory/delivery-status.ts +141 -15
  266. package/src/memory/external-conversation-store.ts +32 -1
  267. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  268. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  269. package/src/memory/graph/tools.ts +6 -37
  270. package/src/memory/invite-store.ts +53 -0
  271. package/src/memory/jobs-worker.ts +21 -1
  272. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  273. package/src/memory/llm-request-log-store.ts +92 -1
  274. package/src/memory/memory-retrospective-constants.ts +28 -0
  275. package/src/memory/memory-retrospective-enqueue.ts +4 -22
  276. package/src/memory/memory-retrospective-job.ts +438 -21
  277. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  278. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  279. package/src/memory/migrations/100-core-tables.ts +1 -0
  280. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  281. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  282. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  283. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  284. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  285. package/src/memory/migrations/253-document-comments.ts +47 -0
  286. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  287. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  288. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  289. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  290. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  291. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  292. package/src/memory/migrations/index.ts +20 -0
  293. package/src/memory/migrations/registry.ts +33 -0
  294. package/src/memory/onboarding-events-store.ts +7 -0
  295. package/src/memory/schema/a2a.ts +15 -0
  296. package/src/memory/schema/calls.ts +1 -0
  297. package/src/memory/schema/conversations.ts +3 -0
  298. package/src/memory/schema/index.ts +1 -0
  299. package/src/memory/schema/inference.ts +2 -0
  300. package/src/memory/schema/infrastructure.ts +2 -0
  301. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  302. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  303. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  304. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  305. package/src/memory/v2/__tests__/injection.test.ts +221 -17
  306. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  307. package/src/memory/v2/__tests__/router.test.ts +489 -1
  308. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  309. package/src/memory/v2/activation-store.ts +14 -16
  310. package/src/memory/v2/cli-command-content.ts +19 -0
  311. package/src/memory/v2/cli-command-store.ts +304 -0
  312. package/src/memory/v2/consolidation-job.ts +14 -0
  313. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  314. package/src/memory/v2/injection-events.ts +101 -0
  315. package/src/memory/v2/injection.ts +69 -29
  316. package/src/memory/v2/page-index.ts +246 -19
  317. package/src/memory/v2/page-store.ts +18 -0
  318. package/src/memory/v2/router.ts +209 -55
  319. package/src/memory/v2/static-context.ts +4 -4
  320. package/src/memory/v2/types.ts +23 -0
  321. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  322. package/src/messaging/providers/a2a/deliver.ts +156 -0
  323. package/src/messaging/providers/gmail/client.ts +9 -2
  324. package/src/messaging/providers/index.ts +18 -3
  325. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  326. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  327. package/src/messaging/providers/slack/adapter.ts +178 -25
  328. package/src/messaging/providers/slack/api.test.ts +54 -0
  329. package/src/messaging/providers/slack/api.ts +119 -3
  330. package/src/messaging/providers/slack/client.ts +12 -0
  331. package/src/messaging/providers/slack/deep-link.ts +20 -1
  332. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  333. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  334. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  335. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  336. package/src/messaging/providers/slack/send.test.ts +77 -0
  337. package/src/messaging/providers/slack/send.ts +8 -2
  338. package/src/messaging/providers/slack/types.ts +14 -0
  339. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  340. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  341. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  342. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +5 -1
  343. package/src/notifications/__tests__/home-feed-side-effect.test.ts +521 -36
  344. package/src/notifications/adapters/macos.ts +12 -2
  345. package/src/notifications/broadcaster.ts +29 -4
  346. package/src/notifications/conversation-seed-composer.ts +14 -2
  347. package/src/notifications/copy-composer.ts +17 -64
  348. package/src/notifications/decision-engine.ts +111 -44
  349. package/src/notifications/deferred-emit.ts +135 -0
  350. package/src/notifications/deterministic-checks.ts +96 -0
  351. package/src/notifications/emit-signal.ts +10 -1
  352. package/src/notifications/home-feed-side-effect.ts +136 -27
  353. package/src/notifications/signal.ts +0 -4
  354. package/src/notifications/types.ts +8 -0
  355. package/src/oauth/connect-orchestrator.ts +3 -0
  356. package/src/oauth/credential-token-resolver.ts +2 -0
  357. package/src/oauth/manual-token-connection.ts +19 -0
  358. package/src/oauth/oauth-store.ts +12 -0
  359. package/src/oauth/platform-connection.test.ts +43 -3
  360. package/src/oauth/platform-connection.ts +13 -4
  361. package/src/oauth/seed-providers.ts +22 -0
  362. package/src/permissions/prompter.ts +5 -2
  363. package/src/permissions/secret-prompter.ts +4 -1
  364. package/src/plugins/defaults/injectors.ts +118 -26
  365. package/src/plugins/external-plugin-loader.ts +82 -10
  366. package/src/plugins/types.ts +16 -7
  367. package/src/prompts/__tests__/system-prompt.test.ts +44 -45
  368. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  369. package/src/prompts/normalize-onboarding.ts +40 -0
  370. package/src/prompts/sections.ts +32 -14
  371. package/src/prompts/system-prompt.ts +105 -76
  372. package/src/prompts/template-detection.ts +37 -0
  373. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  374. package/src/prompts/templates/BOOTSTRAP.md +13 -5
  375. package/src/prompts/templates/VOICE.md +3 -0
  376. package/src/prompts/templates/system-sections.ts +51 -10
  377. package/src/providers/__tests__/inference.test.ts +2 -0
  378. package/src/providers/anthropic/client.ts +132 -5
  379. package/src/providers/call-site-routing.ts +24 -6
  380. package/src/providers/connection-resolution.ts +63 -13
  381. package/src/providers/fireworks/client.ts +20 -2
  382. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  383. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  384. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  385. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  386. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  387. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  388. package/src/providers/inference/adapter-factory.ts +24 -21
  389. package/src/providers/inference/auth.ts +15 -3
  390. package/src/providers/inference/backfill.ts +14 -1
  391. package/src/providers/inference/codex-token-refresh.ts +128 -0
  392. package/src/providers/inference/connections.ts +85 -5
  393. package/src/providers/inference/resolve-auth.ts +50 -5
  394. package/src/providers/model-catalog.ts +244 -242
  395. package/src/providers/model-intents.ts +3 -3
  396. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  397. package/src/providers/openai/chat-completions-provider.ts +215 -25
  398. package/src/providers/openai/responses-provider.ts +9 -3
  399. package/src/providers/openrouter/client.ts +46 -4
  400. package/src/providers/platform-proxy/constants.ts +3 -4
  401. package/src/providers/provider-catalog-visibility.ts +3 -1
  402. package/src/providers/provider-send-message.ts +27 -12
  403. package/src/providers/registry.ts +30 -1
  404. package/src/providers/types.ts +25 -0
  405. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  406. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  407. package/src/runtime/agent-wake.ts +212 -57
  408. package/src/runtime/auth/route-policy.ts +20 -3
  409. package/src/runtime/background-job-runner.ts +26 -0
  410. package/src/runtime/channel-reply-delivery.ts +182 -47
  411. package/src/runtime/channel-retry-sweep.ts +141 -16
  412. package/src/runtime/http-server.ts +7 -16
  413. package/src/runtime/http-types.ts +7 -51
  414. package/src/runtime/pending-interactions.ts +51 -8
  415. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  416. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  417. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +121 -5
  418. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  419. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  420. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  421. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  422. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  423. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  424. package/src/runtime/routes/approval-routes.ts +4 -1
  425. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  426. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  427. package/src/runtime/routes/consolidation-routes.ts +100 -0
  428. package/src/runtime/routes/content-source-routes.ts +78 -0
  429. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  430. package/src/runtime/routes/conversation-query-routes.ts +130 -12
  431. package/src/runtime/routes/conversation-routes.ts +288 -76
  432. package/src/runtime/routes/document-comments-routes.ts +287 -0
  433. package/src/runtime/routes/documents-routes.ts +33 -0
  434. package/src/runtime/routes/home-feed-routes.ts +6 -3
  435. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  436. package/src/runtime/routes/host-browser-routes.ts +8 -1
  437. package/src/runtime/routes/identity-routes.ts +21 -0
  438. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  439. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  440. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  441. package/src/runtime/routes/index.ts +14 -4
  442. package/src/runtime/routes/inference-provider-connection-routes.ts +192 -3
  443. package/src/runtime/routes/integrations/a2a.ts +294 -0
  444. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  445. package/src/runtime/routes/log-export-routes.ts +39 -0
  446. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  447. package/src/runtime/routes/notification-routes.ts +19 -2
  448. package/src/runtime/routes/question-routes.ts +4 -1
  449. package/src/runtime/routes/sanity-routes.ts +159 -0
  450. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  451. package/src/runtime/routes/subagents-routes.ts +41 -0
  452. package/src/runtime/services/conversation-serializer.ts +30 -4
  453. package/src/schedule/integration-status.ts +3 -1
  454. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  455. package/src/security/oauth2-device-code.ts +307 -0
  456. package/src/security/oauth2.ts +26 -9
  457. package/src/security/secure-keys.ts +5 -0
  458. package/src/skills/catalog-install.ts +6 -2
  459. package/src/subagent/manager.ts +2 -0
  460. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  461. package/src/tools/browser/browser-execution.ts +93 -0
  462. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  463. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  464. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  465. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  466. package/src/tools/browser/cdp-client/factory.ts +87 -3
  467. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  468. package/src/tools/browser/cdp-client/types.ts +36 -0
  469. package/src/tools/browser/pinned-tabs.ts +90 -0
  470. package/src/tools/document/document-comment-tool.test.ts +379 -0
  471. package/src/tools/document/document-comment-tool.ts +156 -0
  472. package/src/tools/document/document-tool.ts +128 -2
  473. package/src/tools/memory/register.ts +1 -9
  474. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  475. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  476. package/src/tools/network/domain-normalize.ts +17 -0
  477. package/src/tools/network/web-fetch.ts +213 -64
  478. package/src/tools/network/web-search.ts +191 -66
  479. package/src/tools/registry.ts +2 -2
  480. package/src/tools/terminal/safe-env.ts +3 -2
  481. package/src/tools/tool-approval-handler.ts +19 -12
  482. package/src/tools/types.ts +41 -2
  483. package/src/tools/ui-surface/definitions.ts +3 -1
  484. package/src/types/onboarding-context.ts +4 -0
  485. package/src/util/__tests__/favicon.test.ts +84 -0
  486. package/src/util/favicon.ts +40 -0
  487. package/src/util/platform.ts +0 -5
  488. package/src/workspace/git-service.ts +75 -4
  489. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  490. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  491. package/src/workspace/migrations/registry.ts +4 -0
  492. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  493. package/src/config/bundled-skills/document/SKILL.md +0 -54
  494. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  495. package/src/daemon/seed-files.ts +0 -18
  496. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  497. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
  498. package/src/runtime/routes/interface-routes.ts +0 -43
  499. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  500. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  501. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  502. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  503. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -0,0 +1,597 @@
1
+ /**
2
+ * End-to-end A2A channel integration tests.
3
+ *
4
+ * Tests the assistant-side A2A lifecycle: connection initiation, task store +
5
+ * delivery adapter flow, ACL enforcement via trusted contacts, push
6
+ * notifications, and the feature toggle.
7
+ *
8
+ * Because the gateway and assistant are separate processes, we test the
9
+ * assistant-side integration with mocked HTTP for inter-gateway calls and
10
+ * mocked config for feature flag checks.
11
+ */
12
+
13
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
14
+
15
+ import type { ChannelReplyPayload } from "@vellumai/gateway-client";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Mock state
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const fetchCalls: Array<{ url: string; init: RequestInit }> = [];
22
+ let fetchResponseMap: Record<
23
+ string,
24
+ { ok: boolean; status: number; body: string }
25
+ > = {};
26
+ let defaultFetchResponse = { ok: true, status: 200, body: "{}" };
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Mocks — must be set up before importing modules under test
30
+ // ---------------------------------------------------------------------------
31
+
32
+ mock.module("../../util/logger.js", () => ({
33
+ getLogger: () =>
34
+ new Proxy({} as Record<string, unknown>, {
35
+ get: () => () => {},
36
+ }),
37
+ truncateForLog: (value: string) => value,
38
+ }));
39
+
40
+ // The config-a2a handler uses these config functions directly (not mocked via
41
+ // module replacement) because it calls loadRawConfig/saveRawConfig to toggle
42
+ // the a2a.enabled flag. We use the real config system backed by initializeDb's
43
+ // workspace directory.
44
+
45
+ import {
46
+ invalidateConfigCache,
47
+ loadRawConfig,
48
+ saveRawConfig,
49
+ setNestedValue,
50
+ } from "../../config/loader.js";
51
+ import {
52
+ findContactByAddress,
53
+ upsertContact,
54
+ } from "../../contacts/contact-store.js";
55
+ import {
56
+ clearA2AConfig,
57
+ getA2AConfig,
58
+ setA2AConfig,
59
+ } from "../../daemon/handlers/config-a2a.js";
60
+ import { getSqlite } from "../../memory/db-connection.js";
61
+ import { initializeDb } from "../../memory/db-init.js";
62
+ import type { A2AMessage, Artifact } from "../protocol-types.js";
63
+ import {
64
+ completeWithArtifacts,
65
+ createTask,
66
+ getPushUrl,
67
+ getTask,
68
+ updateState,
69
+ } from "../task-store.js";
70
+
71
+ initializeDb();
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Global fetch intercept
75
+ // ---------------------------------------------------------------------------
76
+
77
+ const originalFetch = globalThis.fetch;
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Helpers
81
+ // ---------------------------------------------------------------------------
82
+
83
+ function resetTables(): void {
84
+ const sqlite = getSqlite();
85
+ sqlite.run("DELETE FROM a2a_tasks");
86
+ sqlite.run("DELETE FROM assistant_contact_metadata");
87
+ sqlite.run("DELETE FROM contact_channels");
88
+ sqlite.run("DELETE FROM contacts");
89
+ }
90
+
91
+ function setConfigEnabled(enabled: boolean): void {
92
+ const raw = loadRawConfig();
93
+ setNestedValue(raw, "a2a.enabled", enabled);
94
+ setNestedValue(raw, "ingress.publicBaseUrl", "https://self.example.com");
95
+ saveRawConfig(raw);
96
+ invalidateConfigCache();
97
+ }
98
+
99
+ function makeRequestMessage(overrides?: Partial<A2AMessage>): A2AMessage {
100
+ return {
101
+ message_id: crypto.randomUUID(),
102
+ role: "user",
103
+ parts: [{ kind: "text", text: "Hello from sender" }],
104
+ ...overrides,
105
+ };
106
+ }
107
+
108
+ function installFetchMock(): void {
109
+ fetchCalls.length = 0;
110
+ fetchResponseMap = {};
111
+ defaultFetchResponse = { ok: true, status: 200, body: "{}" };
112
+
113
+ globalThis.fetch = (async (
114
+ input: string | URL | Request,
115
+ init?: RequestInit,
116
+ ) => {
117
+ const url = typeof input === "string" ? input : input.toString();
118
+ fetchCalls.push({ url, init: init ?? {} });
119
+
120
+ // Check for URL-specific responses
121
+ for (const [pattern, response] of Object.entries(fetchResponseMap)) {
122
+ if (url.includes(pattern)) {
123
+ return new Response(response.body, {
124
+ status: response.status,
125
+ statusText: response.ok ? "OK" : "Error",
126
+ });
127
+ }
128
+ }
129
+
130
+ return new Response(defaultFetchResponse.body, {
131
+ status: defaultFetchResponse.status,
132
+ statusText: defaultFetchResponse.ok ? "OK" : "Error",
133
+ });
134
+ }) as typeof fetch;
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Setup / teardown
139
+ // ---------------------------------------------------------------------------
140
+
141
+ beforeEach(() => {
142
+ resetTables();
143
+ setConfigEnabled(false);
144
+ installFetchMock();
145
+ });
146
+
147
+ afterEach(() => {
148
+ globalThis.fetch = originalFetch;
149
+ });
150
+
151
+ // ===========================================================================
152
+ // Test: trusted contact setup (platform-mediated)
153
+ // ===========================================================================
154
+
155
+ describe("e2e: trusted contact setup", () => {
156
+ test("upsertContact creates a2a channel for assistant contact", () => {
157
+ setConfigEnabled(true);
158
+
159
+ upsertContact({
160
+ displayName: "Peer Assistant",
161
+ contactType: "assistant",
162
+ role: "contact",
163
+ channels: [
164
+ {
165
+ type: "a2a",
166
+ address: "assistant-b",
167
+ externalUserId: "assistant-b",
168
+ status: "active",
169
+ policy: "allow",
170
+ },
171
+ ],
172
+ });
173
+
174
+ const contact = findContactByAddress("a2a", "assistant-b");
175
+ expect(contact).not.toBeNull();
176
+ expect(contact!.channels.some((ch) => ch.type === "a2a")).toBe(true);
177
+ const a2aChannel = contact!.channels.find((ch) => ch.type === "a2a");
178
+ expect(a2aChannel!.status).toBe("active");
179
+ expect(a2aChannel!.address).toBe("assistant-b");
180
+ });
181
+ });
182
+
183
+ // ===========================================================================
184
+ // Test: message after trusted contact established
185
+ // ===========================================================================
186
+
187
+ describe("e2e: message delivery after trusted contact established", () => {
188
+ test("task store lifecycle: create -> working -> complete with artifacts", () => {
189
+ // Create a task as if an inbound A2A message arrived
190
+ const msg = makeRequestMessage({
191
+ parts: [{ kind: "text", text: "Order a coffee for me" }],
192
+ });
193
+
194
+ const task = createTask({
195
+ senderAssistantId: "assistant-a",
196
+ requestMessage: msg,
197
+ pushUrl: "https://requester.example.com/a2a/push",
198
+ });
199
+
200
+ expect(task.status.state).toBe("submitted");
201
+
202
+ // Transition to working
203
+ const working = updateState(task.id, "working", "Processing request...");
204
+ expect(working.status.state).toBe("working");
205
+
206
+ // Complete with artifacts (simulating the assistant's response)
207
+ const artifacts: Artifact[] = [
208
+ {
209
+ artifact_id: crypto.randomUUID(),
210
+ parts: [{ kind: "text", text: "I'll have a latte" }],
211
+ },
212
+ ];
213
+ const completed = completeWithArtifacts(task.id, artifacts);
214
+
215
+ expect(completed.status.state).toBe("completed");
216
+ expect(completed.artifacts).toHaveLength(1);
217
+ expect(completed.artifacts![0].parts[0]).toEqual({
218
+ kind: "text",
219
+ text: "I'll have a latte",
220
+ });
221
+
222
+ // Verify via fresh getTask
223
+ const fetched = getTask(task.id);
224
+ expect(fetched).not.toBeNull();
225
+ expect(fetched!.status.state).toBe("completed");
226
+ expect(fetched!.artifacts).toEqual(completed.artifacts);
227
+
228
+ // Push URL is stored and retrievable
229
+ const pushUrl = getPushUrl(task.id);
230
+ expect(pushUrl).toBe("https://requester.example.com/a2a/push");
231
+ });
232
+
233
+ test("delivery adapter completes task and triggers push notification", async () => {
234
+ // Create a task that simulates an inbound A2A request
235
+ const task = createTask({
236
+ senderAssistantId: "assistant-a",
237
+ requestMessage: makeRequestMessage(),
238
+ pushUrl: "https://requester.example.com/a2a/push",
239
+ });
240
+
241
+ // Move to working state (as the runtime would)
242
+ updateState(task.id, "working");
243
+
244
+ // Import the delivery adapter
245
+ const { deliverA2AReply } =
246
+ await import("../../messaging/providers/a2a/deliver.js");
247
+
248
+ // Simulate the assistant's response via the delivery adapter
249
+ const payload: ChannelReplyPayload = {
250
+ chatId: "chat-1",
251
+ text: "I'll have a latte",
252
+ };
253
+
254
+ const callbackUrl = `https://example.com/deliver/a2a?taskId=${task.id}`;
255
+ const result = await deliverA2AReply(callbackUrl, payload);
256
+
257
+ expect(result.ok).toBe(true);
258
+
259
+ // Task should be completed in the store
260
+ const completedTask = getTask(task.id);
261
+ expect(completedTask).not.toBeNull();
262
+ expect(completedTask!.status.state).toBe("completed");
263
+ expect(completedTask!.artifacts).toHaveLength(1);
264
+ expect(completedTask!.artifacts![0].parts[0]).toEqual({
265
+ kind: "text",
266
+ text: "I'll have a latte",
267
+ });
268
+
269
+ // Wait for the fire-and-forget push notification
270
+ await new Promise((r) => setTimeout(r, 100));
271
+
272
+ // Verify push notification was sent
273
+ const pushCall = fetchCalls.find((c) =>
274
+ c.url.includes("requester.example.com/a2a/push"),
275
+ );
276
+ expect(pushCall).toBeTruthy();
277
+ expect(pushCall!.init.method).toBe("POST");
278
+
279
+ const headers = pushCall!.init.headers as Record<string, string>;
280
+ expect(headers["Content-Type"]).toBe("application/a2a+json");
281
+ expect(headers["A2A-Version"]).toBe("1.0");
282
+
283
+ // Push body should contain the completed task
284
+ const pushBody = JSON.parse(pushCall!.init.body as string);
285
+ expect(pushBody.status.state).toBe("completed");
286
+ expect(pushBody.artifacts).toHaveLength(1);
287
+ });
288
+ });
289
+
290
+ // ===========================================================================
291
+ // Test: disabled channel
292
+ // ===========================================================================
293
+
294
+ describe("e2e: disabled channel", () => {
295
+ test("getA2AConfig returns disabled when a2a.enabled is false", () => {
296
+ const result = getA2AConfig();
297
+ expect(result.success).toBe(true);
298
+ expect(result.enabled).toBe(false);
299
+ });
300
+
301
+ test("clearA2AConfig disables the channel", () => {
302
+ setConfigEnabled(true);
303
+ const before = getA2AConfig();
304
+ expect(before.enabled).toBe(true);
305
+
306
+ const result = clearA2AConfig();
307
+ expect(result.success).toBe(true);
308
+ expect(result.enabled).toBe(false);
309
+
310
+ const after = getA2AConfig();
311
+ expect(after.enabled).toBe(false);
312
+ });
313
+
314
+ test("setA2AConfig enables the channel and clearA2AConfig disables it", () => {
315
+ // Start disabled
316
+ expect(getA2AConfig().enabled).toBe(false);
317
+
318
+ // Enable
319
+ setA2AConfig();
320
+ expect(getA2AConfig().enabled).toBe(true);
321
+
322
+ // Disable
323
+ clearA2AConfig();
324
+ expect(getA2AConfig().enabled).toBe(false);
325
+ });
326
+ });
327
+
328
+ // ===========================================================================
329
+ // Test: unknown sender blocked
330
+ // ===========================================================================
331
+
332
+ describe("e2e: unknown sender blocked (ACL enforcement)", () => {
333
+ test("no trusted contact exists for the sender assistant", () => {
334
+ // Verify there is no contact for the unknown sender
335
+ const contact = findContactByAddress("a2a", "unknown-assistant");
336
+ expect(contact).toBeNull();
337
+
338
+ // Create a task from the unknown sender (as the gateway would)
339
+ const msg = makeRequestMessage({
340
+ parts: [{ kind: "text", text: "Hey, do something for me" }],
341
+ });
342
+ const task = createTask({
343
+ senderAssistantId: "unknown-assistant",
344
+ requestMessage: msg,
345
+ });
346
+
347
+ // The task is created (the gateway always creates a task), but
348
+ // the runtime's ACL check would reject it because there is no
349
+ // trusted contact_channel for "unknown-assistant".
350
+ expect(task.status.state).toBe("submitted");
351
+
352
+ // The ACL layer performs findContactByAddress to resolve trust.
353
+ // For an unknown sender, this returns null — blocking the message.
354
+ const senderContact = findContactByAddress("a2a", "unknown-assistant");
355
+ expect(senderContact).toBeNull();
356
+ });
357
+
358
+ test("trusted contact exists with active a2a channel — ACL passes", async () => {
359
+ const { upsertContact } = await import("../../contacts/contact-store.js");
360
+
361
+ // Pre-create a trusted contact for the sender
362
+ upsertContact({
363
+ displayName: "Trusted Bot",
364
+ contactType: "assistant",
365
+ role: "contact",
366
+ channels: [
367
+ {
368
+ type: "a2a",
369
+ address: "trusted-assistant",
370
+ externalUserId: "trusted-assistant",
371
+ status: "active",
372
+ policy: "allow",
373
+ },
374
+ ],
375
+ });
376
+
377
+ // Verify the contact exists (the ACL check the runtime performs)
378
+ const contact = findContactByAddress("a2a", "trusted-assistant");
379
+ expect(contact).not.toBeNull();
380
+
381
+ const a2aChannel = contact!.channels.find((ch) => ch.type === "a2a");
382
+ expect(a2aChannel).toBeTruthy();
383
+ expect(a2aChannel!.status).toBe("active");
384
+ expect(a2aChannel!.policy).toBe("allow");
385
+
386
+ // A task from this sender would pass the ACL check
387
+ const msg = makeRequestMessage();
388
+ const task = createTask({
389
+ senderAssistantId: "trusted-assistant",
390
+ requestMessage: msg,
391
+ });
392
+ expect(task.status.state).toBe("submitted");
393
+ });
394
+
395
+ test("contact exists but channel is blocked — ACL would reject", async () => {
396
+ const { upsertContact } = await import("../../contacts/contact-store.js");
397
+
398
+ upsertContact({
399
+ displayName: "Blocked Bot",
400
+ contactType: "assistant",
401
+ role: "contact",
402
+ channels: [
403
+ {
404
+ type: "a2a",
405
+ address: "blocked-assistant",
406
+ externalUserId: "blocked-assistant",
407
+ status: "blocked",
408
+ policy: "deny",
409
+ },
410
+ ],
411
+ });
412
+
413
+ const contact = findContactByAddress("a2a", "blocked-assistant");
414
+ expect(contact).not.toBeNull();
415
+
416
+ const a2aChannel = contact!.channels.find((ch) => ch.type === "a2a");
417
+ expect(a2aChannel!.status).toBe("blocked");
418
+ expect(a2aChannel!.policy).toBe("deny");
419
+ });
420
+ });
421
+
422
+ // ===========================================================================
423
+ // Test: push notification failure gracefully degrades
424
+ // ===========================================================================
425
+
426
+ describe("e2e: push notification failure graceful degradation", () => {
427
+ test("task completes even when push URL returns 500", async () => {
428
+ // Set up a task with a push URL that will fail
429
+ const task = createTask({
430
+ senderAssistantId: "assistant-a",
431
+ requestMessage: makeRequestMessage(),
432
+ pushUrl: "https://failing-push.example.com/a2a/push",
433
+ });
434
+ updateState(task.id, "working");
435
+
436
+ // Mock: all push requests fail with 500
437
+ fetchResponseMap["failing-push.example.com"] = {
438
+ ok: false,
439
+ status: 500,
440
+ body: "Internal Server Error",
441
+ };
442
+
443
+ const { deliverA2AReply } =
444
+ await import("../../messaging/providers/a2a/deliver.js");
445
+
446
+ const callbackUrl = `https://example.com/deliver/a2a?taskId=${task.id}`;
447
+ const result = await deliverA2AReply(callbackUrl, {
448
+ chatId: "chat-1",
449
+ text: "Here is your response",
450
+ });
451
+
452
+ // Delivery still succeeds — push failure is fire-and-forget
453
+ expect(result.ok).toBe(true);
454
+
455
+ // Task is completed in the store regardless of push failure
456
+ const completedTask = getTask(task.id);
457
+ expect(completedTask).not.toBeNull();
458
+ expect(completedTask!.status.state).toBe("completed");
459
+ expect(completedTask!.artifacts).toHaveLength(1);
460
+ expect(completedTask!.artifacts![0].parts[0]).toEqual({
461
+ kind: "text",
462
+ text: "Here is your response",
463
+ });
464
+
465
+ // Wait for push retry attempts to fully settle (3 retries with exponential backoff)
466
+ await new Promise((r) => setTimeout(r, 10_000));
467
+
468
+ // Push was attempted (initial + retries)
469
+ const pushCalls = fetchCalls.filter((c) =>
470
+ c.url.includes("failing-push.example.com"),
471
+ );
472
+ expect(pushCalls.length).toBeGreaterThanOrEqual(1);
473
+ }, 15_000);
474
+
475
+ test("task completes when no push URL is configured", async () => {
476
+ const task = createTask({
477
+ senderAssistantId: "assistant-a",
478
+ requestMessage: makeRequestMessage(),
479
+ // No pushUrl
480
+ });
481
+ updateState(task.id, "working");
482
+
483
+ const { deliverA2AReply } =
484
+ await import("../../messaging/providers/a2a/deliver.js");
485
+
486
+ const callbackUrl = `https://example.com/deliver/a2a?taskId=${task.id}`;
487
+ const result = await deliverA2AReply(callbackUrl, {
488
+ chatId: "chat-1",
489
+ text: "Response without push",
490
+ });
491
+
492
+ expect(result.ok).toBe(true);
493
+
494
+ // No push URL means no fetch calls for push
495
+ await new Promise((r) => setTimeout(r, 50));
496
+ expect(fetchCalls).toHaveLength(0);
497
+
498
+ // Task is still completed
499
+ const completedTask = getTask(task.id);
500
+ expect(completedTask!.status.state).toBe("completed");
501
+ });
502
+ });
503
+
504
+ // ===========================================================================
505
+ // Full round-trip: connect -> trusted contact -> send message -> response -> push
506
+ // ===========================================================================
507
+
508
+ describe("e2e: full A2A round-trip", () => {
509
+ test("connect, establish trust, send message, deliver response, push notification", async () => {
510
+ setConfigEnabled(true);
511
+
512
+ // Step 1: Create trusted contact for Assistant B (platform-mediated)
513
+ upsertContact({
514
+ displayName: "Assistant B",
515
+ contactType: "assistant",
516
+ role: "contact",
517
+ channels: [
518
+ {
519
+ type: "a2a",
520
+ address: "assistant-b",
521
+ externalUserId: "assistant-b",
522
+ status: "active",
523
+ policy: "allow",
524
+ },
525
+ ],
526
+ });
527
+
528
+ // Step 2: Verify trusted contact was created
529
+ const contact = findContactByAddress("a2a", "assistant-b");
530
+ expect(contact).not.toBeNull();
531
+ expect(contact!.channels.find((ch) => ch.type === "a2a")!.status).toBe(
532
+ "active",
533
+ );
534
+
535
+ // Step 3: Simulate inbound A2A message from B (as if B sent us a request)
536
+ const inboundMsg = makeRequestMessage({
537
+ parts: [{ kind: "text", text: "Can you help me with something?" }],
538
+ });
539
+ const task = createTask({
540
+ senderAssistantId: "assistant-b",
541
+ requestMessage: inboundMsg,
542
+ pushUrl: "https://b.example.com/a2a/push",
543
+ });
544
+
545
+ expect(task.status.state).toBe("submitted");
546
+
547
+ // ACL check: trusted contact exists
548
+ const senderContact = findContactByAddress("a2a", "assistant-b");
549
+ expect(senderContact).not.toBeNull();
550
+
551
+ // Step 4: Runtime processes the task
552
+ updateState(task.id, "working");
553
+
554
+ // Step 5: Deliver the response via the delivery adapter
555
+ // Clear previous fetch calls
556
+ fetchCalls.length = 0;
557
+ fetchResponseMap = {};
558
+ defaultFetchResponse = { ok: true, status: 200, body: "{}" };
559
+
560
+ const { deliverA2AReply } =
561
+ await import("../../messaging/providers/a2a/deliver.js");
562
+
563
+ const callbackUrl = `https://example.com/deliver/a2a?taskId=${task.id}`;
564
+ const result = await deliverA2AReply(callbackUrl, {
565
+ chatId: "chat-1",
566
+ text: "Sure, I can help!",
567
+ });
568
+
569
+ expect(result.ok).toBe(true);
570
+
571
+ // Step 6: Verify task completed with artifact
572
+ const completedTask = getTask(task.id);
573
+ expect(completedTask!.status.state).toBe("completed");
574
+ expect(completedTask!.artifacts).toHaveLength(1);
575
+ expect(completedTask!.artifacts![0].parts[0]).toEqual({
576
+ kind: "text",
577
+ text: "Sure, I can help!",
578
+ });
579
+
580
+ // Step 7: Verify push notification was sent to B
581
+ await new Promise((r) => setTimeout(r, 100));
582
+
583
+ const pushCall = fetchCalls.find((c) =>
584
+ c.url.includes("b.example.com/a2a/push"),
585
+ );
586
+ expect(pushCall).toBeTruthy();
587
+ expect(pushCall!.init.method).toBe("POST");
588
+
589
+ const pushHeaders = pushCall!.init.headers as Record<string, string>;
590
+ expect(pushHeaders["Content-Type"]).toBe("application/a2a+json");
591
+ expect(pushHeaders["A2A-Version"]).toBe("1.0");
592
+
593
+ const pushBody = JSON.parse(pushCall!.init.body as string);
594
+ expect(pushBody.status.state).toBe("completed");
595
+ expect(pushBody.artifacts).toHaveLength(1);
596
+ });
597
+ });
@@ -0,0 +1,113 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { TERMINAL_TASK_STATES } from "../protocol-constants.js";
4
+ import {
5
+ INTERNAL_ERROR,
6
+ INVALID_PARAMS,
7
+ INVALID_REQUEST,
8
+ makeJsonRpcError,
9
+ makeJsonRpcSuccess,
10
+ METHOD_NOT_FOUND,
11
+ PARSE_ERROR,
12
+ TASK_NOT_CANCELABLE,
13
+ TASK_NOT_FOUND,
14
+ UNSUPPORTED_OPERATION,
15
+ } from "../protocol-errors.js";
16
+
17
+ describe("makeJsonRpcError", () => {
18
+ test("produces a valid JSON-RPC 2.0 error response", () => {
19
+ const response = makeJsonRpcError(1, PARSE_ERROR, "Parse error");
20
+
21
+ expect(response).toEqual({
22
+ jsonrpc: "2.0",
23
+ id: 1,
24
+ error: {
25
+ code: -32700,
26
+ message: "Parse error",
27
+ },
28
+ });
29
+ });
30
+
31
+ test("includes data field when provided", () => {
32
+ const response = makeJsonRpcError(
33
+ "req-1",
34
+ INVALID_PARAMS,
35
+ "Invalid params",
36
+ { field: "message" },
37
+ );
38
+
39
+ expect(response).toEqual({
40
+ jsonrpc: "2.0",
41
+ id: "req-1",
42
+ error: {
43
+ code: -32602,
44
+ message: "Invalid params",
45
+ data: { field: "message" },
46
+ },
47
+ });
48
+ });
49
+
50
+ test("omits data field when not provided", () => {
51
+ const response = makeJsonRpcError(null, INTERNAL_ERROR, "Internal error");
52
+
53
+ expect(response.error).toBeDefined();
54
+ expect("data" in response.error!).toBe(false);
55
+ });
56
+
57
+ test("accepts null id", () => {
58
+ const response = makeJsonRpcError(null, PARSE_ERROR, "Parse error");
59
+
60
+ expect(response.id).toBeNull();
61
+ });
62
+ });
63
+
64
+ describe("makeJsonRpcSuccess", () => {
65
+ test("produces a valid JSON-RPC 2.0 success response", () => {
66
+ const response = makeJsonRpcSuccess(1, { status: "ok" });
67
+
68
+ expect(response).toEqual({
69
+ jsonrpc: "2.0",
70
+ id: 1,
71
+ result: { status: "ok" },
72
+ });
73
+ });
74
+
75
+ test("does not include an error field", () => {
76
+ const response = makeJsonRpcSuccess("req-2", null);
77
+
78
+ expect("error" in response).toBe(false);
79
+ expect(response.result).toBeNull();
80
+ });
81
+ });
82
+
83
+ describe("error code constants", () => {
84
+ test("standard JSON-RPC codes", () => {
85
+ expect(PARSE_ERROR).toBe(-32700);
86
+ expect(INVALID_REQUEST).toBe(-32600);
87
+ expect(METHOD_NOT_FOUND).toBe(-32601);
88
+ expect(INVALID_PARAMS).toBe(-32602);
89
+ expect(INTERNAL_ERROR).toBe(-32603);
90
+ });
91
+
92
+ test("A2A-specific codes", () => {
93
+ expect(TASK_NOT_FOUND).toBe(-32001);
94
+ expect(TASK_NOT_CANCELABLE).toBe(-32002);
95
+ expect(UNSUPPORTED_OPERATION).toBe(-32004);
96
+ });
97
+ });
98
+
99
+ describe("TERMINAL_TASK_STATES", () => {
100
+ test("contains exactly the four terminal states", () => {
101
+ expect(TERMINAL_TASK_STATES.size).toBe(4);
102
+ expect(TERMINAL_TASK_STATES.has("completed")).toBe(true);
103
+ expect(TERMINAL_TASK_STATES.has("failed")).toBe(true);
104
+ expect(TERMINAL_TASK_STATES.has("canceled")).toBe(true);
105
+ expect(TERMINAL_TASK_STATES.has("rejected")).toBe(true);
106
+ });
107
+
108
+ test("does not contain non-terminal states", () => {
109
+ expect(TERMINAL_TASK_STATES.has("submitted")).toBe(false);
110
+ expect(TERMINAL_TASK_STATES.has("working")).toBe(false);
111
+ expect(TERMINAL_TASK_STATES.has("input_required")).toBe(false);
112
+ });
113
+ });