@vellumai/assistant 0.8.3 → 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 (342) hide show
  1. package/docker-entrypoint.sh +0 -1
  2. package/node_modules/@vellumai/gateway-client/src/types.ts +2 -0
  3. package/openapi.yaml +610 -16
  4. package/package.json +1 -1
  5. package/src/__tests__/agent-loop-exit-reason.test.ts +4 -5
  6. package/src/__tests__/agent-loop-override-profile.test.ts +1 -1
  7. package/src/__tests__/agent-loop.test.ts +88 -3
  8. package/src/__tests__/anthropic-provider.test.ts +272 -0
  9. package/src/__tests__/approval-cascade.test.ts +1 -1
  10. package/src/__tests__/background-workers-disk-pressure.test.ts +2 -1
  11. package/src/__tests__/channel-delivery-store.test.ts +193 -0
  12. package/src/__tests__/channel-reply-delivery.test.ts +284 -5
  13. package/src/__tests__/channel-retry-sweep.test.ts +274 -1
  14. package/src/__tests__/compaction-events.test.ts +1 -1
  15. package/src/__tests__/compactor-preserved-tail-count.test.ts +110 -0
  16. package/src/__tests__/config-watcher.test.ts +1 -1
  17. package/src/__tests__/context-token-estimator.test.ts +91 -1
  18. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  19. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +54 -3
  20. package/src/__tests__/conversation-agent-loop-overflow.test.ts +31 -6
  21. package/src/__tests__/conversation-agent-loop.test.ts +25 -7
  22. package/src/__tests__/conversation-app-control-lifecycle.test.ts +1 -1
  23. package/src/__tests__/conversation-clean-command.test.ts +137 -0
  24. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  25. package/src/__tests__/conversation-fork-crud.test.ts +161 -0
  26. package/src/__tests__/conversation-lifecycle.test.ts +1 -1
  27. package/src/__tests__/conversation-load-cleaned-at.test.ts +279 -0
  28. package/src/__tests__/conversation-load-history-repair.test.ts +1 -1
  29. package/src/__tests__/conversation-pairing.test.ts +2 -2
  30. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  31. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  32. package/src/__tests__/conversation-queue.test.ts +1 -1
  33. package/src/__tests__/conversation-runtime-assembly.test.ts +264 -81
  34. package/src/__tests__/conversation-seed-composer.test.ts +66 -4
  35. package/src/__tests__/conversation-slash-commands.test.ts +36 -8
  36. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  37. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  38. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  39. package/src/__tests__/conversation-surfaces-task-progress.test.ts +220 -0
  40. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  41. package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
  42. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
  43. package/src/__tests__/credential-security-invariants.test.ts +6 -0
  44. package/src/__tests__/cu-unified-flow.test.ts +10 -1
  45. package/src/__tests__/dm-backfill.test.ts +64 -0
  46. package/src/__tests__/dm-persistence.test.ts +33 -0
  47. package/src/__tests__/document-find-replace.test.ts +501 -0
  48. package/src/__tests__/first-greeting.test.ts +23 -2
  49. package/src/__tests__/headless-browser-navigate.test.ts +172 -0
  50. package/src/__tests__/host-bash-proxy.test.ts +6 -0
  51. package/src/__tests__/host-browser-proxy.test.ts +10 -0
  52. package/src/__tests__/host-cu-proxy.test.ts +8 -1
  53. package/src/__tests__/host-file-proxy.test.ts +8 -1
  54. package/src/__tests__/host-transfer-proxy.test.ts +8 -1
  55. package/src/__tests__/identity-routes.test.ts +57 -0
  56. package/src/__tests__/inbound-slack-persistence.test.ts +3 -0
  57. package/src/__tests__/injector-chain.test.ts +2 -0
  58. package/src/__tests__/injector-document-comments.test.ts +378 -0
  59. package/src/__tests__/injector-pkb-v2-silenced.test.ts +4 -25
  60. package/src/__tests__/list-messages-attachments.test.ts +21 -17
  61. package/src/__tests__/list-messages-hidden-metadata.test.ts +217 -0
  62. package/src/__tests__/list-messages-page-latest.test.ts +130 -14
  63. package/src/__tests__/list-messages-tool-merge.test.ts +17 -16
  64. package/src/__tests__/llm-context-normalization.test.ts +0 -2
  65. package/src/__tests__/llm-resolver.test.ts +85 -1
  66. package/src/__tests__/log-export-routes.test.ts +99 -2
  67. package/src/__tests__/message-queue-steer.test.ts +114 -0
  68. package/src/__tests__/openai-provider.test.ts +105 -0
  69. package/src/__tests__/openai-responses-provider.test.ts +4 -4
  70. package/src/__tests__/outbound-slack-persistence.test.ts +187 -20
  71. package/src/__tests__/pending-interactions-resolved-event.test.ts +190 -0
  72. package/src/__tests__/platform.test.ts +0 -3
  73. package/src/__tests__/plugin-source-watcher.test.ts +302 -0
  74. package/src/__tests__/process-message-background-slack.test.ts +1 -51
  75. package/src/__tests__/process-message-display-content.test.ts +21 -16
  76. package/src/__tests__/server-history-render.test.ts +83 -4
  77. package/src/__tests__/steer-tool-repair.test.ts +249 -0
  78. package/src/__tests__/system-prompt.test.ts +51 -28
  79. package/src/__tests__/terminal-tools.test.ts +11 -1
  80. package/src/__tests__/thinking-block-replay.test.ts +113 -0
  81. package/src/__tests__/thread-backfill.test.ts +370 -22
  82. package/src/__tests__/tool-executor.test.ts +90 -1
  83. package/src/__tests__/tool-result-metadata-plumbing.test.ts +167 -0
  84. package/src/__tests__/twilio-routes.test.ts +1 -1
  85. package/src/__tests__/web-fetch.test.ts +2 -2
  86. package/src/__tests__/workspace-git-service.test.ts +88 -5
  87. package/src/__tests__/workspace-migration-088-deprecate-background-conversation-override.test.ts +158 -0
  88. package/src/agent/attachments.ts +1 -0
  89. package/src/agent/loop.ts +57 -20
  90. package/src/background-wake/next-wake.test.ts +289 -0
  91. package/src/background-wake/next-wake.ts +172 -0
  92. package/src/browser/operations.ts +15 -0
  93. package/src/cli/commands/__tests__/conversations-slack.test.ts +572 -0
  94. package/src/cli/commands/__tests__/memory-v2.test.ts +9 -12
  95. package/src/cli/commands/conversations.ts +128 -1
  96. package/src/cli/commands/inference-providers.ts +147 -1
  97. package/src/cli/commands/memory-v2.ts +308 -0
  98. package/src/cli/commands/notifications.ts +24 -2
  99. package/src/cli/utils/conversation-id.ts +17 -5
  100. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  101. package/src/config/bundled-skills/document-editor/SKILL.md +115 -0
  102. package/src/config/bundled-skills/document-editor/TOOLS.json +240 -0
  103. package/src/config/bundled-skills/document-editor/tools/comment-list.ts +12 -0
  104. package/src/config/bundled-skills/document-editor/tools/comment-reply.ts +12 -0
  105. package/src/config/bundled-skills/document-editor/tools/comment-resolve.ts +12 -0
  106. package/src/config/bundled-skills/document-editor/tools/document-find.ts +12 -0
  107. package/src/config/bundled-skills/document-editor/tools/document-replace-text.ts +12 -0
  108. package/src/config/bundled-skills/media-processing/SKILL.md +8 -0
  109. package/src/config/bundled-skills/schedule/SKILL.md +8 -0
  110. package/src/config/bundled-tool-registry.ts +22 -12
  111. package/src/config/call-site-defaults.ts +19 -0
  112. package/src/config/feature-flag-registry.json +99 -3
  113. package/src/config/llm-resolver.ts +16 -2
  114. package/src/config/schemas/__tests__/memory-v2.test.ts +4 -0
  115. package/src/config/schemas/call-site-catalog.ts +21 -0
  116. package/src/config/schemas/llm.ts +3 -0
  117. package/src/config/schemas/memory-v2.ts +48 -1
  118. package/src/context/compactor.ts +8 -1
  119. package/src/context/token-estimator.ts +47 -4
  120. package/src/context/window-manager.ts +25 -0
  121. package/src/credential-health/credential-health-service.ts +34 -19
  122. package/src/daemon/__tests__/conversation-tool-setup.test.ts +66 -6
  123. package/src/daemon/__tests__/native-web-search-metadata.test.ts +357 -0
  124. package/src/daemon/__tests__/web-search-status-text.test.ts +287 -0
  125. package/src/daemon/conversation-agent-loop-handlers.ts +153 -23
  126. package/src/daemon/conversation-agent-loop.ts +223 -54
  127. package/src/daemon/conversation-lifecycle.ts +142 -116
  128. package/src/daemon/conversation-messaging.ts +3 -0
  129. package/src/daemon/conversation-process.ts +273 -0
  130. package/src/daemon/conversation-queue-manager.ts +14 -0
  131. package/src/daemon/conversation-runtime-assembly.ts +135 -75
  132. package/src/daemon/conversation-slash.ts +37 -5
  133. package/src/daemon/conversation-surfaces.ts +45 -2
  134. package/src/daemon/conversation-tool-setup.ts +7 -0
  135. package/src/daemon/conversation.ts +42 -5
  136. package/src/daemon/first-greeting.ts +10 -0
  137. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +498 -0
  138. package/src/daemon/handlers/config-a2a.ts +160 -0
  139. package/src/daemon/handlers/config-model.test.ts +1 -0
  140. package/src/daemon/handlers/conversations.ts +79 -0
  141. package/src/daemon/handlers/shared.ts +92 -29
  142. package/src/daemon/host-bash-proxy.ts +1 -1
  143. package/src/daemon/host-cu-proxy.ts +1 -1
  144. package/src/daemon/host-file-proxy.ts +1 -1
  145. package/src/daemon/host-transfer-proxy.ts +1 -1
  146. package/src/daemon/lifecycle.ts +18 -4
  147. package/src/daemon/message-protocol.ts +4 -0
  148. package/src/daemon/message-types/conversations.ts +8 -0
  149. package/src/daemon/message-types/document-comments.ts +50 -0
  150. package/src/daemon/message-types/messages.ts +68 -1
  151. package/src/daemon/message-types/surfaces.ts +3 -1
  152. package/src/daemon/message-types/web-activity.ts +57 -0
  153. package/src/daemon/plugin-source-watcher.ts +135 -3
  154. package/src/daemon/process-message.ts +69 -12
  155. package/src/daemon/query-complexity-router.ts +75 -0
  156. package/src/daemon/trust-context.ts +6 -0
  157. package/src/documents/document-comments-store.test.ts +338 -0
  158. package/src/documents/document-comments-store.ts +237 -0
  159. package/src/documents/document-store.ts +202 -0
  160. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -1
  161. package/src/heartbeat/heartbeat-service.ts +1 -0
  162. package/src/home/__tests__/suggested-prompts.test.ts +33 -2
  163. package/src/home/feed-types.ts +6 -1
  164. package/src/home/home-content-refresh.ts +52 -0
  165. package/src/home/home-greeting-cache.ts +69 -0
  166. package/src/home/home-greeting.ts +94 -0
  167. package/src/home/suggested-prompts.ts +177 -9
  168. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +135 -2
  169. package/src/memory/__tests__/memory-retrospective-job.test.ts +320 -6
  170. package/src/memory/conversation-crud.ts +133 -43
  171. package/src/memory/db-init.ts +16 -0
  172. package/src/memory/delivery-crud.ts +41 -0
  173. package/src/memory/delivery-status.ts +141 -15
  174. package/src/memory/external-conversation-store.ts +32 -1
  175. package/src/memory/jobs-worker.ts +21 -1
  176. package/src/memory/memory-retrospective-constants.ts +28 -0
  177. package/src/memory/memory-retrospective-enqueue.ts +3 -2
  178. package/src/memory/memory-retrospective-job.ts +408 -18
  179. package/src/memory/memory-retrospective-startup-cleanup.ts +3 -3
  180. package/src/memory/memory-v2-activation-log-store.ts +26 -8
  181. package/src/memory/migrations/100-core-tables.ts +1 -0
  182. package/src/memory/migrations/109-external-conversation-bindings.ts +1 -0
  183. package/src/memory/migrations/253-conversation-last-notified-profile.ts +15 -0
  184. package/src/memory/migrations/253-document-comments.ts +47 -0
  185. package/src/memory/migrations/254-external-conversation-binding-chat-name.ts +43 -0
  186. package/src/memory/migrations/255-channel-inbound-delivery-attempts.ts +24 -0
  187. package/src/memory/migrations/256-memory-v2-injection-events.ts +113 -0
  188. package/src/memory/migrations/257-strip-base-url-non-openai-compatible.ts +22 -0
  189. package/src/memory/migrations/258-onboarding-events-prior-assistants.ts +13 -0
  190. package/src/memory/migrations/259-conversation-cleaned-at.ts +33 -0
  191. package/src/memory/migrations/index.ts +17 -0
  192. package/src/memory/migrations/registry.ts +25 -0
  193. package/src/memory/onboarding-events-store.ts +7 -0
  194. package/src/memory/schema/calls.ts +1 -0
  195. package/src/memory/schema/conversations.ts +3 -0
  196. package/src/memory/schema/infrastructure.ts +1 -0
  197. package/src/memory/v2/__tests__/injection-events.test.ts +318 -0
  198. package/src/memory/v2/__tests__/injection.test.ts +31 -14
  199. package/src/memory/v2/__tests__/page-index.test.ts +365 -1
  200. package/src/memory/v2/__tests__/router.test.ts +489 -1
  201. package/src/memory/v2/consolidation-job.ts +14 -0
  202. package/src/memory/v2/injection-events.ts +101 -0
  203. package/src/memory/v2/injection.ts +21 -10
  204. package/src/memory/v2/page-index.ts +209 -7
  205. package/src/memory/v2/page-store.ts +18 -0
  206. package/src/memory/v2/router.ts +209 -55
  207. package/src/messaging/providers/index.ts +7 -1
  208. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +329 -3
  209. package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +34 -1
  210. package/src/messaging/providers/slack/adapter.ts +178 -25
  211. package/src/messaging/providers/slack/api.test.ts +54 -0
  212. package/src/messaging/providers/slack/api.ts +119 -3
  213. package/src/messaging/providers/slack/client.ts +12 -0
  214. package/src/messaging/providers/slack/deep-link.ts +20 -1
  215. package/src/messaging/providers/slack/message-metadata.test.ts +48 -0
  216. package/src/messaging/providers/slack/message-metadata.ts +156 -0
  217. package/src/messaging/providers/slack/render-transcript.test.ts +107 -75
  218. package/src/messaging/providers/slack/render-transcript.ts +176 -49
  219. package/src/messaging/providers/slack/send.test.ts +77 -0
  220. package/src/messaging/providers/slack/send.ts +8 -2
  221. package/src/messaging/providers/slack/types.ts +14 -0
  222. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +4 -1
  223. package/src/notifications/__tests__/home-feed-side-effect.test.ts +116 -54
  224. package/src/notifications/conversation-seed-composer.ts +14 -2
  225. package/src/notifications/deferred-emit.ts +135 -0
  226. package/src/notifications/emit-signal.ts +9 -1
  227. package/src/notifications/home-feed-side-effect.ts +60 -30
  228. package/src/oauth/connect-orchestrator.ts +3 -0
  229. package/src/oauth/credential-token-resolver.ts +2 -0
  230. package/src/oauth/manual-token-connection.ts +19 -0
  231. package/src/oauth/oauth-store.ts +12 -0
  232. package/src/oauth/seed-providers.ts +22 -0
  233. package/src/permissions/prompter.ts +5 -2
  234. package/src/permissions/secret-prompter.ts +4 -1
  235. package/src/plugins/defaults/injectors.ts +82 -9
  236. package/src/prompts/__tests__/system-prompt.test.ts +46 -2
  237. package/src/prompts/normalize-onboarding.ts +40 -0
  238. package/src/prompts/sections.ts +32 -14
  239. package/src/prompts/system-prompt.ts +105 -68
  240. package/src/prompts/template-detection.ts +37 -0
  241. package/src/prompts/templates/BOOTSTRAP-CONTENT-AUTOMATION.md +141 -0
  242. package/src/prompts/templates/BOOTSTRAP.md +8 -0
  243. package/src/prompts/templates/VOICE.md +3 -0
  244. package/src/prompts/templates/system-sections.ts +53 -3
  245. package/src/providers/anthropic/client.ts +132 -5
  246. package/src/providers/fireworks/client.ts +20 -2
  247. package/src/providers/inference/__tests__/base-url-route-validation.test.ts +342 -0
  248. package/src/providers/inference/__tests__/base-url-security.test.ts +189 -0
  249. package/src/providers/inference/__tests__/codex-token-refresh.test.ts +254 -0
  250. package/src/providers/inference/adapter-factory.ts +15 -1
  251. package/src/providers/inference/auth.ts +3 -3
  252. package/src/providers/inference/codex-token-refresh.ts +128 -0
  253. package/src/providers/inference/resolve-auth.ts +49 -6
  254. package/src/providers/model-catalog.ts +48 -1
  255. package/src/providers/openai/chat-completions-provider.ts +57 -20
  256. package/src/providers/openai/responses-provider.ts +9 -3
  257. package/src/providers/openrouter/client.ts +5 -1
  258. package/src/providers/types.ts +25 -0
  259. package/src/runtime/__tests__/agent-wake.test.ts +214 -0
  260. package/src/runtime/__tests__/background-job-runner.test.ts +128 -0
  261. package/src/runtime/agent-wake.ts +151 -56
  262. package/src/runtime/auth/route-policy.ts +7 -3
  263. package/src/runtime/background-job-runner.ts +26 -0
  264. package/src/runtime/channel-reply-delivery.ts +182 -47
  265. package/src/runtime/channel-retry-sweep.ts +141 -16
  266. package/src/runtime/http-types.ts +7 -4
  267. package/src/runtime/pending-interactions.ts +51 -8
  268. package/src/runtime/routes/__tests__/content-source-routes.test.ts +162 -0
  269. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +55 -1
  270. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +14 -0
  271. package/src/runtime/routes/__tests__/memory-v2-simulate-route.test.ts +271 -0
  272. package/src/runtime/routes/__tests__/sanity-routes.test.ts +280 -0
  273. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +266 -0
  274. package/src/runtime/routes/approval-routes.ts +4 -1
  275. package/src/runtime/routes/chatgpt-subscription-auth-routes.ts +246 -0
  276. package/src/runtime/routes/content-source-routes.ts +78 -0
  277. package/src/runtime/routes/conversation-cli-routes.ts +146 -1
  278. package/src/runtime/routes/conversation-query-routes.ts +60 -1
  279. package/src/runtime/routes/conversation-routes.ts +281 -76
  280. package/src/runtime/routes/document-comments-routes.ts +287 -0
  281. package/src/runtime/routes/documents-routes.ts +33 -0
  282. package/src/runtime/routes/home-feed-routes.ts +6 -3
  283. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  284. package/src/runtime/routes/host-browser-routes.ts +8 -1
  285. package/src/runtime/routes/identity-routes.ts +21 -0
  286. package/src/runtime/routes/inbound-message-handler.ts +288 -58
  287. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +365 -6
  288. package/src/runtime/routes/inbound-stages/background-dispatch.ts +283 -82
  289. package/src/runtime/routes/index.ts +12 -4
  290. package/src/runtime/routes/inference-provider-connection-routes.ts +63 -7
  291. package/src/runtime/routes/integrations/a2a.ts +60 -1
  292. package/src/runtime/routes/log-export-routes.ts +39 -0
  293. package/src/runtime/routes/memory-v2-routes.ts +217 -0
  294. package/src/runtime/routes/notification-routes.ts +19 -2
  295. package/src/runtime/routes/question-routes.ts +4 -1
  296. package/src/runtime/routes/sanity-routes.ts +159 -0
  297. package/src/runtime/routes/slack-channel-routes.ts +187 -0
  298. package/src/runtime/services/conversation-serializer.ts +30 -4
  299. package/src/schedule/integration-status.ts +3 -1
  300. package/src/security/__tests__/oauth2-device-code.test.ts +479 -0
  301. package/src/security/oauth2-device-code.ts +307 -0
  302. package/src/security/oauth2.ts +26 -9
  303. package/src/security/secure-keys.ts +5 -0
  304. package/src/skills/catalog-install.ts +6 -2
  305. package/src/tools/browser/__tests__/pinned-tabs.test.ts +80 -0
  306. package/src/tools/browser/browser-execution.ts +93 -0
  307. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +28 -0
  308. package/src/tools/browser/cdp-client/__tests__/types.test.ts +1 -0
  309. package/src/tools/browser/cdp-client/cdp-inspect-client.ts +10 -0
  310. package/src/tools/browser/cdp-client/extension-cdp-client.ts +15 -1
  311. package/src/tools/browser/cdp-client/factory.ts +87 -3
  312. package/src/tools/browser/cdp-client/local-cdp-client.ts +9 -0
  313. package/src/tools/browser/cdp-client/types.ts +36 -0
  314. package/src/tools/browser/pinned-tabs.ts +90 -0
  315. package/src/tools/document/document-comment-tool.test.ts +379 -0
  316. package/src/tools/document/document-comment-tool.ts +156 -0
  317. package/src/tools/document/document-tool.ts +128 -2
  318. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +229 -0
  319. package/src/tools/network/__tests__/web-search-metadata.test.ts +346 -0
  320. package/src/tools/network/domain-normalize.ts +17 -0
  321. package/src/tools/network/web-fetch.ts +213 -64
  322. package/src/tools/network/web-search.ts +191 -66
  323. package/src/tools/terminal/safe-env.ts +3 -2
  324. package/src/tools/tool-approval-handler.ts +19 -12
  325. package/src/tools/types.ts +4 -0
  326. package/src/tools/ui-surface/definitions.ts +3 -1
  327. package/src/types/onboarding-context.ts +4 -0
  328. package/src/util/__tests__/favicon.test.ts +84 -0
  329. package/src/util/favicon.ts +40 -0
  330. package/src/util/platform.ts +0 -5
  331. package/src/workspace/git-service.ts +75 -4
  332. package/src/workspace/migrations/088-deprecate-background-conversation-override.ts +103 -0
  333. package/src/workspace/migrations/registry.ts +2 -0
  334. package/src/config/bundled-skills/document/SKILL.md +0 -54
  335. package/src/config/bundled-skills/document/TOOLS.json +0 -106
  336. package/src/daemon/seed-files.ts +0 -18
  337. package/src/runtime/routes/interface-routes.ts +0 -43
  338. /package/src/config/bundled-skills/{document → document-editor}/tools/document-create.ts +0 -0
  339. /package/src/config/bundled-skills/{document → document-editor}/tools/document-delete.ts +0 -0
  340. /package/src/config/bundled-skills/{document → document-editor}/tools/document-list.ts +0 -0
  341. /package/src/config/bundled-skills/{document → document-editor}/tools/document-read.ts +0 -0
  342. /package/src/config/bundled-skills/{document → document-editor}/tools/document-update.ts +0 -0
@@ -102,6 +102,16 @@ function resolveTone(raw?: string): Tone {
102
102
  return raw && VALID_TONES.has(raw) ? (raw as Tone) : "grounded";
103
103
  }
104
104
 
105
+ export function buildScanFirstMessage(
106
+ url: string,
107
+ variant: "website" | "content-source",
108
+ ): string {
109
+ if (variant === "content-source") {
110
+ return `Here's a page with content I'd like you to look at: ${url}`;
111
+ }
112
+ return `Here's my website: ${url}`;
113
+ }
114
+
105
115
  function buildPersonalizedGreeting(ctx: OnboardingGreetingContext): string {
106
116
  const name = ctx.userName?.trim();
107
117
  const assistant = ctx.assistantName?.trim();
@@ -0,0 +1,498 @@
1
+ /**
2
+ * Tests for the self-hosted A2A invite accept broker (acceptA2AInvite).
3
+ *
4
+ * Uses the real DB (via `initializeDb()`) and the test preload which sets
5
+ * `VELLUM_WORKSPACE_DIR` to a per-file temp directory. Global `fetch` is
6
+ * mocked to simulate the outbound call to the sender's gateway.
7
+ */
8
+
9
+ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
10
+
11
+ mock.module("../../../util/logger.js", () => ({
12
+ getLogger: () =>
13
+ new Proxy({} as Record<string, unknown>, {
14
+ get: () => () => {},
15
+ }),
16
+ }));
17
+
18
+ import {
19
+ invalidateConfigCache,
20
+ loadRawConfig,
21
+ saveRawConfig,
22
+ setNestedValue,
23
+ } from "../../../config/loader.js";
24
+ import {
25
+ getAssistantContactMetadata,
26
+ getContact,
27
+ searchContacts,
28
+ } from "../../../contacts/contact-store.js";
29
+ import { getSqlite } from "../../../memory/db-connection.js";
30
+ import { initializeDb } from "../../../memory/db-init.js";
31
+ import { acceptA2AInvite, createA2AInvite } from "../config-a2a.js";
32
+
33
+ initializeDb();
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Helpers
37
+ // ---------------------------------------------------------------------------
38
+
39
+ function resetTables(): void {
40
+ const sqlite = getSqlite();
41
+ sqlite.run("DELETE FROM assistant_ingress_invites");
42
+ sqlite.run("DELETE FROM assistant_contact_metadata");
43
+ sqlite.run("DELETE FROM contact_channels");
44
+ sqlite.run("DELETE FROM contacts");
45
+ }
46
+
47
+ function setConfig(opts: {
48
+ a2aEnabled?: boolean;
49
+ publicBaseUrl?: string;
50
+ ingressEnabled?: boolean;
51
+ assistantName?: string;
52
+ }): void {
53
+ const raw = loadRawConfig();
54
+ if (opts.a2aEnabled !== undefined) {
55
+ setNestedValue(raw, "a2a.enabled", opts.a2aEnabled);
56
+ }
57
+ if (opts.publicBaseUrl !== undefined) {
58
+ setNestedValue(raw, "ingress.publicBaseUrl", opts.publicBaseUrl);
59
+ }
60
+ if (opts.ingressEnabled !== undefined) {
61
+ setNestedValue(raw, "ingress.enabled", opts.ingressEnabled);
62
+ }
63
+ saveRawConfig(raw);
64
+ invalidateConfigCache();
65
+ }
66
+
67
+ const SENDER_GATEWAY_URL = "https://sender.example.com";
68
+ const SENDER_ASSISTANT_ID = "sender-assistant-abc";
69
+ const RECEIVER_GATEWAY_URL = "https://receiver.example.com";
70
+
71
+ interface MockFetchOptions {
72
+ status?: number;
73
+ body?: Record<string, unknown>;
74
+ networkError?: string;
75
+ }
76
+
77
+ function mockFetchOnce(opts: MockFetchOptions): void {
78
+ const originalFetch = globalThis.fetch;
79
+ const mockFn = mock(
80
+ async (_input: RequestInfo | URL, _init?: RequestInit) => {
81
+ // Restore after first call
82
+ globalThis.fetch = originalFetch;
83
+
84
+ if (opts.networkError) {
85
+ throw new Error(opts.networkError);
86
+ }
87
+ return new Response(JSON.stringify(opts.body ?? {}), {
88
+ status: opts.status ?? 200,
89
+ headers: { "Content-Type": "application/json" },
90
+ });
91
+ },
92
+ );
93
+ globalThis.fetch = mockFn as unknown as typeof fetch;
94
+ }
95
+
96
+ /** Track the outbound fetch call and return a mock response. */
97
+ function mockFetchCapture(opts: MockFetchOptions): {
98
+ getCall: () => { url: string; body: Record<string, unknown> } | null;
99
+ } {
100
+ const originalFetch = globalThis.fetch;
101
+ let captured: { url: string; body: Record<string, unknown> } | null = null;
102
+ const mockFn = mock(async (input: RequestInfo | URL, init?: RequestInit) => {
103
+ globalThis.fetch = originalFetch;
104
+ captured = {
105
+ url: String(input),
106
+ body: JSON.parse((init?.body as string) ?? "{}") as Record<
107
+ string,
108
+ unknown
109
+ >,
110
+ };
111
+ if (opts.networkError) {
112
+ throw new Error(opts.networkError);
113
+ }
114
+ return new Response(JSON.stringify(opts.body ?? {}), {
115
+ status: opts.status ?? 200,
116
+ headers: { "Content-Type": "application/json" },
117
+ });
118
+ });
119
+ globalThis.fetch = mockFn as unknown as typeof fetch;
120
+ return { getCall: () => captured };
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Tests
125
+ // ---------------------------------------------------------------------------
126
+
127
+ describe("acceptA2AInvite", () => {
128
+ let savedFetch: typeof globalThis.fetch;
129
+
130
+ beforeEach(() => {
131
+ savedFetch = globalThis.fetch;
132
+ resetTables();
133
+ setConfig({
134
+ a2aEnabled: true,
135
+ publicBaseUrl: RECEIVER_GATEWAY_URL,
136
+ ingressEnabled: true,
137
+ });
138
+ });
139
+
140
+ afterEach(() => {
141
+ globalThis.fetch = savedFetch;
142
+ });
143
+
144
+ // ── Happy path ──────────────────────────────────────────────────────
145
+
146
+ test("happy path: creates local contact from sender identity", async () => {
147
+ // Create an invite on the sender side so we have a valid token
148
+ const created = createA2AInvite({});
149
+ expect(created.success).toBe(true);
150
+
151
+ mockFetchOnce({
152
+ body: {
153
+ success: true,
154
+ sender: {
155
+ assistantId: SENDER_ASSISTANT_ID,
156
+ displayName: "Sender Bot",
157
+ gatewayUrl: SENDER_GATEWAY_URL,
158
+ },
159
+ },
160
+ });
161
+
162
+ const result = await acceptA2AInvite({
163
+ senderGatewayUrl: SENDER_GATEWAY_URL,
164
+ senderAssistantId: SENDER_ASSISTANT_ID,
165
+ token: "any-token",
166
+ });
167
+
168
+ expect(result.success).toBe(true);
169
+ expect(result.contactId).toBeDefined();
170
+ expect(result.alreadyConnected).toBeFalsy();
171
+
172
+ // Verify the contact was created with correct identity
173
+ const contact = getContact(result.contactId!);
174
+ expect(contact).not.toBeNull();
175
+ expect(contact!.channels).toHaveLength(1);
176
+ expect(contact!.channels[0]!.type).toBe("a2a");
177
+ expect(contact!.channels[0]!.status).toBe("active");
178
+
179
+ // Verify assistant metadata
180
+ const metadata = getAssistantContactMetadata(result.contactId!);
181
+ expect(metadata).not.toBeNull();
182
+ expect(metadata!.metadata).toEqual({
183
+ assistantId: SENDER_ASSISTANT_ID,
184
+ gatewayUrl: SENDER_GATEWAY_URL,
185
+ });
186
+ });
187
+
188
+ test("uses invite-link values for sender identity, not daemon response", async () => {
189
+ const maliciousGateway = "https://evil.example.com";
190
+ const maliciousId = "evil-assistant";
191
+
192
+ mockFetchOnce({
193
+ body: {
194
+ success: true,
195
+ sender: {
196
+ // Sender daemon tries to misrepresent identity
197
+ assistantId: maliciousId,
198
+ displayName: "Legit Bot",
199
+ gatewayUrl: maliciousGateway,
200
+ },
201
+ },
202
+ });
203
+
204
+ const result = await acceptA2AInvite({
205
+ senderGatewayUrl: SENDER_GATEWAY_URL,
206
+ senderAssistantId: SENDER_ASSISTANT_ID,
207
+ token: "any-token",
208
+ });
209
+
210
+ expect(result.success).toBe(true);
211
+
212
+ // Verify contact uses invite-link values, NOT the daemon response
213
+ const metadata = getAssistantContactMetadata(result.contactId!);
214
+ expect(metadata!.metadata).toEqual({
215
+ assistantId: SENDER_ASSISTANT_ID,
216
+ gatewayUrl: SENDER_GATEWAY_URL,
217
+ });
218
+ });
219
+
220
+ test("uses sender displayName from complete response", async () => {
221
+ mockFetchOnce({
222
+ body: {
223
+ success: true,
224
+ sender: {
225
+ assistantId: SENDER_ASSISTANT_ID,
226
+ displayName: "My Cool Bot",
227
+ gatewayUrl: SENDER_GATEWAY_URL,
228
+ },
229
+ },
230
+ });
231
+
232
+ const result = await acceptA2AInvite({
233
+ senderGatewayUrl: SENDER_GATEWAY_URL,
234
+ senderAssistantId: SENDER_ASSISTANT_ID,
235
+ token: "any-token",
236
+ });
237
+
238
+ expect(result.success).toBe(true);
239
+ const contact = getContact(result.contactId!);
240
+ expect(contact!.displayName).toBe("My Cool Bot");
241
+ });
242
+
243
+ test("falls back to senderAssistantId when displayName is missing", async () => {
244
+ mockFetchOnce({
245
+ body: {
246
+ success: true,
247
+ sender: {
248
+ assistantId: SENDER_ASSISTANT_ID,
249
+ // No displayName
250
+ gatewayUrl: SENDER_GATEWAY_URL,
251
+ },
252
+ },
253
+ });
254
+
255
+ const result = await acceptA2AInvite({
256
+ senderGatewayUrl: SENDER_GATEWAY_URL,
257
+ senderAssistantId: SENDER_ASSISTANT_ID,
258
+ token: "any-token",
259
+ });
260
+
261
+ expect(result.success).toBe(true);
262
+ const contact = getContact(result.contactId!);
263
+ expect(contact!.displayName).toBe(SENDER_ASSISTANT_ID);
264
+ });
265
+
266
+ test("sends correct request to sender's invite/complete endpoint", async () => {
267
+ const capture = mockFetchCapture({
268
+ body: {
269
+ success: true,
270
+ sender: {
271
+ assistantId: SENDER_ASSISTANT_ID,
272
+ displayName: "Sender Bot",
273
+ gatewayUrl: SENDER_GATEWAY_URL,
274
+ },
275
+ },
276
+ });
277
+
278
+ await acceptA2AInvite({
279
+ senderGatewayUrl: SENDER_GATEWAY_URL,
280
+ senderAssistantId: SENDER_ASSISTANT_ID,
281
+ token: "test-token-123",
282
+ });
283
+
284
+ const call = capture.getCall();
285
+ expect(call).not.toBeNull();
286
+ expect(call!.url).toBe(
287
+ `${SENDER_GATEWAY_URL}/v1/integrations/a2a/invite/complete`,
288
+ );
289
+ expect(call!.body).toEqual({
290
+ token: "test-token-123",
291
+ senderAssistantId: SENDER_ASSISTANT_ID,
292
+ acceptor: {
293
+ assistantId: RECEIVER_GATEWAY_URL,
294
+ displayName: "Vellum Assistant",
295
+ gatewayUrl: RECEIVER_GATEWAY_URL,
296
+ },
297
+ });
298
+ });
299
+
300
+ test("strips trailing slashes from senderGatewayUrl in fetch URL and stored metadata", async () => {
301
+ const capture = mockFetchCapture({
302
+ body: {
303
+ success: true,
304
+ sender: {
305
+ assistantId: SENDER_ASSISTANT_ID,
306
+ displayName: "Bot",
307
+ gatewayUrl: SENDER_GATEWAY_URL,
308
+ },
309
+ },
310
+ });
311
+
312
+ const result = await acceptA2AInvite({
313
+ senderGatewayUrl: `${SENDER_GATEWAY_URL}///`,
314
+ senderAssistantId: SENDER_ASSISTANT_ID,
315
+ token: "test-token",
316
+ });
317
+
318
+ // Fetch URL is normalized
319
+ const call = capture.getCall();
320
+ expect(call!.url).toBe(
321
+ `${SENDER_GATEWAY_URL}/v1/integrations/a2a/invite/complete`,
322
+ );
323
+
324
+ // Stored contact metadata is also normalized (no trailing slashes)
325
+ expect(result.success).toBe(true);
326
+ const contact = getContact(result.contactId!);
327
+ expect(contact).toBeTruthy();
328
+ const meta = getAssistantContactMetadata(result.contactId!);
329
+ expect((meta?.metadata as { gatewayUrl?: string } | null)?.gatewayUrl).toBe(
330
+ SENDER_GATEWAY_URL,
331
+ );
332
+ });
333
+
334
+ // ── Already connected ───────────────────────────────────────────────
335
+
336
+ test("returns alreadyConnected without calling sender when already a contact", async () => {
337
+ // First accept — creates the contact
338
+ mockFetchOnce({
339
+ body: {
340
+ success: true,
341
+ sender: {
342
+ assistantId: SENDER_ASSISTANT_ID,
343
+ displayName: "Sender Bot",
344
+ gatewayUrl: SENDER_GATEWAY_URL,
345
+ },
346
+ },
347
+ });
348
+ const first = await acceptA2AInvite({
349
+ senderGatewayUrl: SENDER_GATEWAY_URL,
350
+ senderAssistantId: SENDER_ASSISTANT_ID,
351
+ token: "token-1",
352
+ });
353
+ expect(first.success).toBe(true);
354
+
355
+ // Second accept — should short-circuit before any outbound call.
356
+ // No mockFetchOnce here: if fetch is called, it hits the real
357
+ // (unmocked) fetch and the test would fail or hang.
358
+ const second = await acceptA2AInvite({
359
+ senderGatewayUrl: SENDER_GATEWAY_URL,
360
+ senderAssistantId: SENDER_ASSISTANT_ID,
361
+ token: "token-2",
362
+ });
363
+ expect(second.success).toBe(true);
364
+ expect(second.alreadyConnected).toBe(true);
365
+ });
366
+
367
+ // ── Sender unreachable ──────────────────────────────────────────────
368
+
369
+ test("returns sender_unreachable when fetch throws", async () => {
370
+ mockFetchOnce({ networkError: "Connection refused" });
371
+
372
+ const result = await acceptA2AInvite({
373
+ senderGatewayUrl: SENDER_GATEWAY_URL,
374
+ senderAssistantId: SENDER_ASSISTANT_ID,
375
+ token: "any-token",
376
+ });
377
+
378
+ expect(result.success).toBe(false);
379
+ expect(result.errorCode).toBe("sender_unreachable");
380
+ expect(result.error).toContain("Connection refused");
381
+ });
382
+
383
+ // ── Sender returns error ────────────────────────────────────────────
384
+ // The daemon HTTP adapter always converts RouteError throws into the
385
+ // standard envelope: { error: { code, message } } (see http-errors.ts).
386
+ // These mocks match the real wire format.
387
+
388
+ test("returns complete_failed when sender returns 400 with token error", async () => {
389
+ mockFetchOnce({
390
+ status: 400,
391
+ body: {
392
+ error: {
393
+ code: "BAD_REQUEST",
394
+ message: "Invite token has expired or was already claimed",
395
+ },
396
+ },
397
+ });
398
+
399
+ const result = await acceptA2AInvite({
400
+ senderGatewayUrl: SENDER_GATEWAY_URL,
401
+ senderAssistantId: SENDER_ASSISTANT_ID,
402
+ token: "expired-token",
403
+ });
404
+
405
+ expect(result.success).toBe(false);
406
+ expect(result.error).toBe(
407
+ "Invite token has expired or was already claimed",
408
+ );
409
+ expect(result.errorCode).toBe("complete_failed");
410
+ });
411
+
412
+ test("returns complete_failed when sender returns 400 for validation", async () => {
413
+ mockFetchOnce({
414
+ status: 400,
415
+ body: {
416
+ error: {
417
+ code: "BAD_REQUEST",
418
+ message:
419
+ "acceptor must include non-empty assistantId, displayName, and gatewayUrl",
420
+ },
421
+ },
422
+ });
423
+
424
+ const result = await acceptA2AInvite({
425
+ senderGatewayUrl: SENDER_GATEWAY_URL,
426
+ senderAssistantId: SENDER_ASSISTANT_ID,
427
+ token: "any-token",
428
+ });
429
+
430
+ expect(result.success).toBe(false);
431
+ expect(result.error).toContain("acceptor must include");
432
+ expect(result.errorCode).toBe("complete_failed");
433
+ });
434
+
435
+ test("falls back to generic message when error envelope is malformed", async () => {
436
+ mockFetchOnce({
437
+ status: 500,
438
+ body: { unexpected: "shape" },
439
+ });
440
+
441
+ const result = await acceptA2AInvite({
442
+ senderGatewayUrl: SENDER_GATEWAY_URL,
443
+ senderAssistantId: SENDER_ASSISTANT_ID,
444
+ token: "any-token",
445
+ });
446
+
447
+ expect(result.success).toBe(false);
448
+ expect(result.error).toBe("Invite completion failed");
449
+ expect(result.errorCode).toBe("complete_failed");
450
+ });
451
+
452
+ // ── No public base URL ──────────────────────────────────────────────
453
+
454
+ test("returns no_public_url when publicBaseUrl is not configured", async () => {
455
+ setConfig({ publicBaseUrl: "", ingressEnabled: true });
456
+
457
+ const result = await acceptA2AInvite({
458
+ senderGatewayUrl: SENDER_GATEWAY_URL,
459
+ senderAssistantId: SENDER_ASSISTANT_ID,
460
+ token: "any-token",
461
+ });
462
+
463
+ expect(result.success).toBe(false);
464
+ expect(result.errorCode).toBe("no_public_url");
465
+ expect(result.error).toContain("public base URL");
466
+ });
467
+
468
+ test("returns no_public_url when ingress is disabled", async () => {
469
+ setConfig({ ingressEnabled: false });
470
+
471
+ const result = await acceptA2AInvite({
472
+ senderGatewayUrl: SENDER_GATEWAY_URL,
473
+ senderAssistantId: SENDER_ASSISTANT_ID,
474
+ token: "any-token",
475
+ });
476
+
477
+ expect(result.success).toBe(false);
478
+ expect(result.errorCode).toBe("no_public_url");
479
+ });
480
+
481
+ // ── No existing contacts leaked ─────────────────────────────────────
482
+
483
+ test("does not create a contact when sender returns failure", async () => {
484
+ mockFetchOnce({
485
+ status: 400,
486
+ body: { error: { code: "BAD_REQUEST", message: "Invalid token" } },
487
+ });
488
+
489
+ await acceptA2AInvite({
490
+ senderGatewayUrl: SENDER_GATEWAY_URL,
491
+ senderAssistantId: SENDER_ASSISTANT_ID,
492
+ token: "bad-token",
493
+ });
494
+
495
+ const contacts = searchContacts({ channelType: "a2a" });
496
+ expect(contacts).toHaveLength(0);
497
+ });
498
+ });
@@ -5,7 +5,9 @@
5
5
  * - setA2AConfig() — set a2a.enabled = true
6
6
  * - clearA2AConfig() — set a2a.enabled = false
7
7
  * - createA2AInvite() — create a shareable invite token for link-based contact creation
8
+ * - completeA2AInvite() — sender-side: claim token and return sender identity
8
9
  * - redeemA2AInvite() — receiver-side: create trusted contact from sender identity
10
+ * - acceptA2AInvite() — self-hosted broker: orchestrate complete + redeem across daemons
9
11
  */
10
12
 
11
13
  import {
@@ -29,7 +31,11 @@ import {
29
31
  hashToken,
30
32
  } from "../../memory/invite-store.js";
31
33
  import { assistantContactMetadata } from "../../memory/schema.js";
34
+ import type { HttpErrorResponse } from "../../runtime/http-errors.js";
35
+ import { getLogger } from "../../util/logger.js";
32
36
  import { getAssistantName } from "../identity-helpers.js";
37
+ const log = getLogger("config-a2a");
38
+
33
39
  // ── Result types ────────────────────────────────────────────────────
34
40
 
35
41
  export interface A2AConfigResult {
@@ -61,6 +67,14 @@ export interface RedeemA2AInviteResult {
61
67
  error?: string;
62
68
  }
63
69
 
70
+ export interface AcceptA2AInviteResult {
71
+ success: boolean;
72
+ contactId?: string;
73
+ alreadyConnected?: boolean;
74
+ error?: string;
75
+ errorCode?: string;
76
+ }
77
+
64
78
  // ── Config operations ───────────────────────────────────────────────
65
79
 
66
80
  export function getA2AConfig(): A2AConfigResult {
@@ -287,3 +301,149 @@ export function redeemA2AInvite(params: {
287
301
 
288
302
  return { success: true, contactId: contact.id };
289
303
  }
304
+
305
+ // ── Self-hosted broker ──────────────────────────────────────────────
306
+
307
+ const ACCEPT_TIMEOUT_MS = 15_000;
308
+
309
+ /**
310
+ * Extract a human-readable error message from a daemon HTTP error
311
+ * response. The daemon always returns `{ error: { code, message } }`
312
+ * (see `HttpErrorResponse` in `runtime/http-errors.ts`).
313
+ */
314
+ function extractDaemonErrorMessage(
315
+ body: Record<string, unknown>,
316
+ ): string | undefined {
317
+ const envelope = body as Partial<HttpErrorResponse>;
318
+ if (
319
+ typeof envelope.error === "object" &&
320
+ envelope.error !== null &&
321
+ typeof envelope.error.message === "string"
322
+ ) {
323
+ return envelope.error.message;
324
+ }
325
+ return undefined;
326
+ }
327
+
328
+ /**
329
+ * Orchestrate cross-daemon A2A invite acceptance for self-hosted
330
+ * deployments. Calls the sender's `invite/complete` endpoint, then
331
+ * creates a local contact via `redeemA2AInvite`.
332
+ *
333
+ * Trust model: the user explicitly chose to connect to `senderGatewayUrl`
334
+ * and provided a token from the sender's invite link. We trust the
335
+ * invite-link values (`senderAssistantId`, `senderGatewayUrl`) as the
336
+ * canonical sender identity, and only use the `complete` response for the
337
+ * sender's display name (which has no other source in self-hosted mode).
338
+ */
339
+ export async function acceptA2AInvite(params: {
340
+ senderGatewayUrl: string;
341
+ senderAssistantId: string;
342
+ token: string;
343
+ }): Promise<AcceptA2AInviteResult> {
344
+ const senderGatewayUrl = params.senderGatewayUrl.replace(/\/+$/, "");
345
+
346
+ // 1. Validate local config
347
+ const displayName = getAssistantName() ?? "Vellum Assistant";
348
+ let localGatewayUrl: string;
349
+ try {
350
+ localGatewayUrl = getPublicBaseUrl(getConfig());
351
+ } catch {
352
+ return {
353
+ success: false,
354
+ error:
355
+ "No public base URL configured. Set ingress.publicBaseUrl in config.",
356
+ errorCode: "no_public_url",
357
+ };
358
+ }
359
+
360
+ // 2. Short-circuit if already connected — avoids a network round-trip
361
+ // and consuming a token on the sender side.
362
+ const existing = findContactByAddress("a2a", params.senderAssistantId);
363
+ if (
364
+ existing &&
365
+ existing.channels.some((ch) => ch.type === "a2a" && ch.status === "active")
366
+ ) {
367
+ return { success: true, alreadyConnected: true, contactId: existing.id };
368
+ }
369
+
370
+ // 3. Call the sender's invite/complete endpoint
371
+ const completeUrl = `${senderGatewayUrl}/v1/integrations/a2a/invite/complete`;
372
+ const completeBody = {
373
+ token: params.token,
374
+ senderAssistantId: params.senderAssistantId,
375
+ acceptor: {
376
+ assistantId: localGatewayUrl,
377
+ displayName,
378
+ gatewayUrl: localGatewayUrl,
379
+ },
380
+ };
381
+
382
+ let completeData: Record<string, unknown>;
383
+ try {
384
+ const response = await fetch(completeUrl, {
385
+ method: "POST",
386
+ headers: { "Content-Type": "application/json" },
387
+ body: JSON.stringify(completeBody),
388
+ signal: AbortSignal.timeout(ACCEPT_TIMEOUT_MS),
389
+ });
390
+
391
+ completeData = (await response.json()) as Record<string, unknown>;
392
+
393
+ if (!response.ok) {
394
+ const error =
395
+ extractDaemonErrorMessage(completeData) ?? "Invite completion failed";
396
+ log.warn(
397
+ { senderGatewayUrl, status: response.status, error },
398
+ "Sender invite/complete returned error",
399
+ );
400
+ return { success: false, error, errorCode: "complete_failed" };
401
+ }
402
+ } catch (err) {
403
+ const message = err instanceof Error ? err.message : String(err);
404
+ log.warn(
405
+ { senderGatewayUrl, error: message },
406
+ "Failed to reach sender for invite/complete",
407
+ );
408
+ return {
409
+ success: false,
410
+ error: `Failed to reach sender: ${message}`,
411
+ errorCode: "sender_unreachable",
412
+ };
413
+ }
414
+
415
+ // 4. Extract sender display name from the complete response; use
416
+ // invite-link values for assistantId and gatewayUrl (trusted source).
417
+ const senderFromResponse = completeData.sender as
418
+ | { displayName?: string }
419
+ | undefined;
420
+
421
+ const senderIdentity = {
422
+ assistantId: params.senderAssistantId,
423
+ displayName:
424
+ (typeof senderFromResponse?.displayName === "string" &&
425
+ senderFromResponse.displayName) ||
426
+ params.senderAssistantId,
427
+ gatewayUrl: senderGatewayUrl,
428
+ };
429
+
430
+ // 5. Create the sender as a local trusted contact
431
+ const redeemResult = redeemA2AInvite({ sender: senderIdentity });
432
+ if (!redeemResult.success) {
433
+ log.warn(
434
+ { error: redeemResult.error },
435
+ "Local invite/redeem failed after successful complete",
436
+ );
437
+ return {
438
+ success: false,
439
+ error: redeemResult.error ?? "Failed to create sender contact",
440
+ errorCode: "redeem_failed",
441
+ };
442
+ }
443
+
444
+ return {
445
+ success: true,
446
+ contactId: redeemResult.contactId,
447
+ alreadyConnected: redeemResult.alreadyConnected,
448
+ };
449
+ }
@@ -65,6 +65,7 @@ describe("projectProviderForWire", () => {
65
65
  const wire = projectProviderForWire(gemini!);
66
66
  const modelIds = wire.models.map((model) => model.id);
67
67
  const expectedGemini3ModelIds = [
68
+ "gemini-3.5-flash",
68
69
  "gemini-3.1-pro-preview",
69
70
  "gemini-3.1-pro-preview-customtools",
70
71
  "gemini-3-flash-preview",