@vellumai/assistant 0.4.29 → 0.4.31

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 (237) hide show
  1. package/ARCHITECTURE.md +39 -37
  2. package/Dockerfile +14 -8
  3. package/README.md +7 -8
  4. package/docs/architecture/memory.md +28 -29
  5. package/docs/runbook-trusted-contacts.md +76 -43
  6. package/package.json +1 -1
  7. package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
  8. package/scripts/test.sh +1 -1
  9. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
  10. package/src/__tests__/actor-token-service.test.ts +4 -3
  11. package/src/__tests__/app-executors.test.ts +7 -17
  12. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
  13. package/src/__tests__/browser-skill-endstate.test.ts +10 -1
  14. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
  15. package/src/__tests__/channel-approval-routes.test.ts +44 -44
  16. package/src/__tests__/channel-approval.test.ts +8 -0
  17. package/src/__tests__/channel-approvals.test.ts +39 -1
  18. package/src/__tests__/channel-guardian.test.ts +15 -5
  19. package/src/__tests__/channel-reply-delivery.test.ts +31 -0
  20. package/src/__tests__/config-schema.test.ts +0 -9
  21. package/src/__tests__/conflict-policy.test.ts +76 -0
  22. package/src/__tests__/conflict-store.test.ts +14 -20
  23. package/src/__tests__/contacts-tools.test.ts +8 -61
  24. package/src/__tests__/contradiction-checker.test.ts +5 -1
  25. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
  26. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  27. package/src/__tests__/gemini-image-service.test.ts +2 -2
  28. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  29. package/src/__tests__/guardian-grant-minting.test.ts +6 -6
  30. package/src/__tests__/guardian-routing-invariants.test.ts +40 -15
  31. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
  32. package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
  33. package/src/__tests__/integrations-cli.test.ts +3 -27
  34. package/src/__tests__/intent-routing.test.ts +3 -0
  35. package/src/__tests__/invite-redemption-service.test.ts +1 -1
  36. package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
  37. package/src/__tests__/ipc-snapshot.test.ts +4 -31
  38. package/src/__tests__/memory-lifecycle-e2e.test.ts +11 -10
  39. package/src/__tests__/nl-approval-parser.test.ts +305 -0
  40. package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
  41. package/src/__tests__/provider-error-scenarios.test.ts +68 -0
  42. package/src/__tests__/registry.test.ts +0 -10
  43. package/src/__tests__/relay-server.test.ts +1 -1
  44. package/src/__tests__/retry-after-extraction.test.ts +111 -0
  45. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
  46. package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
  47. package/src/__tests__/session-agent-loop.test.ts +0 -2
  48. package/src/__tests__/session-conflict-gate.test.ts +243 -388
  49. package/src/__tests__/session-media-retry.test.ts +147 -0
  50. package/src/__tests__/session-profile-injection.test.ts +0 -2
  51. package/src/__tests__/session-runtime-assembly.test.ts +2 -3
  52. package/src/__tests__/session-skill-tools.test.ts +0 -49
  53. package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
  54. package/src/__tests__/session-workspace-injection.test.ts +0 -1
  55. package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
  56. package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
  57. package/src/__tests__/skill-feature-flags.test.ts +18 -12
  58. package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
  59. package/src/__tests__/slack-block-formatting.test.ts +100 -0
  60. package/src/__tests__/slack-inbound-verification.test.ts +346 -0
  61. package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
  62. package/src/__tests__/slack-skill.test.ts +3 -2
  63. package/src/__tests__/starter-task-flow.test.ts +0 -1
  64. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
  65. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
  66. package/src/__tests__/trusted-contact-verification.test.ts +3 -1
  67. package/src/__tests__/voice-invite-redemption.test.ts +1 -1
  68. package/src/amazon/client.ts +7 -24
  69. package/src/approvals/guardian-decision-primitive.ts +11 -7
  70. package/src/approvals/guardian-request-resolvers.ts +5 -3
  71. package/src/calls/relay-server.ts +44 -11
  72. package/src/channels/config.ts +1 -1
  73. package/src/cli/integrations.ts +10 -66
  74. package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
  75. package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
  76. package/src/config/bundled-skills/browser/TOOLS.json +59 -2
  77. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
  78. package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
  79. package/src/config/bundled-skills/contacts/SKILL.md +49 -53
  80. package/src/config/bundled-skills/contacts/TOOLS.json +26 -22
  81. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +40 -62
  82. package/src/config/bundled-skills/contacts/tools/contact-search.ts +17 -43
  83. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +18 -57
  84. package/src/config/bundled-skills/document/TOOLS.json +8 -0
  85. package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
  86. package/src/config/bundled-skills/followups/TOOLS.json +12 -0
  87. package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
  88. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
  89. package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
  90. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
  91. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
  92. package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
  93. package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
  94. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
  95. package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
  96. package/src/config/bundled-skills/notifications/SKILL.md +3 -2
  97. package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
  98. package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
  99. package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
  100. package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
  101. package/src/config/bundled-skills/schedule/SKILL.md +33 -15
  102. package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
  103. package/src/config/bundled-skills/slack/SKILL.md +30 -1
  104. package/src/config/bundled-skills/slack/TOOLS.json +89 -2
  105. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
  106. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
  107. package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
  108. package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
  109. package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
  110. package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
  111. package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
  112. package/src/config/bundled-tool-registry.ts +2 -5
  113. package/src/config/channel-permission-profiles.ts +155 -0
  114. package/src/config/env.ts +4 -1
  115. package/src/config/memory-schema.ts +0 -10
  116. package/src/config/system-prompt.ts +6 -0
  117. package/src/contacts/contact-store.ts +221 -56
  118. package/src/contacts/contacts-write.ts +14 -3
  119. package/src/contacts/types.ts +35 -4
  120. package/src/daemon/assistant-attachments.ts +23 -3
  121. package/src/daemon/guardian-verification-intent.ts +7 -4
  122. package/src/daemon/handlers/apps.ts +1 -2
  123. package/src/daemon/handlers/config-heartbeat.ts +1 -2
  124. package/src/daemon/handlers/config-inbox.ts +16 -134
  125. package/src/daemon/handlers/contacts.ts +2 -2
  126. package/src/daemon/handlers/guardian-actions.ts +21 -88
  127. package/src/daemon/handlers/sessions.ts +2 -2
  128. package/src/daemon/ipc-contract/apps.ts +0 -1
  129. package/src/daemon/ipc-contract/contacts.ts +2 -2
  130. package/src/daemon/ipc-contract/inbox.ts +7 -66
  131. package/src/daemon/ipc-contract/sessions.ts +1 -0
  132. package/src/daemon/ipc-contract/surfaces.ts +0 -1
  133. package/src/daemon/ipc-contract-inventory.json +2 -4
  134. package/src/daemon/lifecycle.ts +14 -2
  135. package/src/daemon/session-agent-loop-handlers.ts +9 -0
  136. package/src/daemon/session-agent-loop.ts +2 -45
  137. package/src/daemon/session-attachments.ts +5 -1
  138. package/src/daemon/session-conflict-gate.ts +21 -82
  139. package/src/daemon/session-error.ts +18 -0
  140. package/src/daemon/session-lifecycle.ts +4 -5
  141. package/src/daemon/session-media-retry.ts +15 -1
  142. package/src/daemon/session-memory.ts +7 -52
  143. package/src/daemon/session-process.ts +3 -1
  144. package/src/daemon/session-runtime-assembly.ts +18 -35
  145. package/src/daemon/session-surfaces.ts +0 -1
  146. package/src/daemon/session-tool-setup.ts +7 -4
  147. package/src/events/domain-events.ts +2 -1
  148. package/src/heartbeat/heartbeat-service.ts +5 -1
  149. package/src/home-base/prebuilt/seed.ts +0 -1
  150. package/src/influencer/client.ts +7 -24
  151. package/src/media/gemini-image-service.ts +48 -3
  152. package/src/memory/app-store.ts +0 -4
  153. package/src/memory/conflict-intent.ts +3 -6
  154. package/src/memory/conflict-policy.ts +34 -0
  155. package/src/memory/conflict-store.ts +10 -18
  156. package/src/memory/contradiction-checker.ts +2 -2
  157. package/src/memory/conversation-attention-store.ts +3 -1
  158. package/src/memory/db-init.ts +8 -0
  159. package/src/memory/job-handlers/conflict.ts +0 -7
  160. package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
  161. package/src/memory/migrations/134-contacts-notes-column.ts +51 -0
  162. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
  163. package/src/memory/migrations/index.ts +3 -0
  164. package/src/memory/schema.ts +12 -17
  165. package/src/memory/slack-thread-store.ts +187 -0
  166. package/src/messaging/index.ts +0 -1
  167. package/src/messaging/providers/slack/client.ts +84 -26
  168. package/src/messaging/providers/slack/types.ts +4 -0
  169. package/src/messaging/types.ts +0 -38
  170. package/src/notifications/adapters/slack.ts +90 -0
  171. package/src/notifications/destination-resolver.ts +42 -1
  172. package/src/notifications/emit-signal.ts +17 -1
  173. package/src/oauth/provider-profiles.ts +22 -0
  174. package/src/providers/anthropic/client.ts +3 -0
  175. package/src/providers/openai/client.ts +3 -0
  176. package/src/providers/retry.ts +9 -1
  177. package/src/runtime/actor-trust-resolver.ts +8 -0
  178. package/src/runtime/auth/require-bound-guardian.ts +44 -0
  179. package/src/runtime/auth/route-policy.ts +4 -8
  180. package/src/runtime/channel-approval-types.ts +18 -0
  181. package/src/runtime/channel-approvals.ts +8 -0
  182. package/src/runtime/channel-invite-transport.ts +1 -1
  183. package/src/runtime/channel-reply-delivery.ts +62 -3
  184. package/src/runtime/gateway-client.ts +36 -2
  185. package/src/runtime/gateway-internal-client.ts +86 -0
  186. package/src/runtime/guardian-action-service.ts +128 -0
  187. package/src/runtime/guardian-outbound-actions.ts +3 -3
  188. package/src/runtime/guardian-reply-router.ts +4 -4
  189. package/src/runtime/guardian-verification-templates.ts +16 -1
  190. package/src/runtime/http-server.ts +29 -46
  191. package/src/runtime/invite-redemption-service.ts +1 -1
  192. package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
  193. package/src/runtime/nl-approval-parser.ts +138 -0
  194. package/src/runtime/routes/approval-routes.ts +1 -40
  195. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
  196. package/src/runtime/routes/channel-route-shared.ts +35 -1
  197. package/src/runtime/routes/contact-routes.ts +494 -47
  198. package/src/runtime/routes/conversation-routes.ts +2 -1
  199. package/src/runtime/routes/global-search-routes.ts +2 -2
  200. package/src/runtime/routes/guardian-action-routes.ts +19 -111
  201. package/src/runtime/routes/guardian-approval-interception.ts +78 -1
  202. package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -1
  203. package/src/runtime/routes/inbound-message-handler.ts +40 -12
  204. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +227 -1
  205. package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
  206. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
  207. package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
  208. package/src/runtime/routes/migration-routes.ts +17 -17
  209. package/src/runtime/slack-block-formatting.ts +176 -0
  210. package/src/schedule/scheduler.ts +11 -2
  211. package/src/tools/apps/executors.ts +16 -15
  212. package/src/tools/calls/call-end.ts +1 -1
  213. package/src/tools/computer-use/definitions.ts +16 -0
  214. package/src/tools/credentials/vault.ts +86 -2
  215. package/src/tools/network/script-proxy/session-manager.ts +28 -3
  216. package/src/tools/permission-checker.ts +18 -0
  217. package/src/tools/terminal/shell.ts +15 -5
  218. package/src/tools/tool-approval-handler.ts +48 -4
  219. package/src/tools/types.ts +38 -1
  220. package/src/util/errors.ts +5 -1
  221. package/src/util/retry.ts +21 -0
  222. package/src/watcher/providers/slack.ts +33 -3
  223. package/src/workspace/git-service.ts +6 -4
  224. package/src/__tests__/get-weather.test.ts +0 -393
  225. package/src/__tests__/weather-skill-regression.test.ts +0 -276
  226. package/src/autonomy/autonomy-resolver.ts +0 -62
  227. package/src/autonomy/autonomy-store.ts +0 -138
  228. package/src/autonomy/disposition-mapper.ts +0 -31
  229. package/src/autonomy/index.ts +0 -11
  230. package/src/autonomy/types.ts +0 -43
  231. package/src/config/bundled-skills/weather/SKILL.md +0 -38
  232. package/src/config/bundled-skills/weather/TOOLS.json +0 -32
  233. package/src/config/bundled-skills/weather/icon.svg +0 -24
  234. package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
  235. package/src/messaging/triage-engine.ts +0 -344
  236. package/src/tools/weather/service.ts +0 -712
  237. /package/src/memory/{ingress-invite-store.ts → invite-store.ts} +0 -0
@@ -10,7 +10,7 @@ Manage the user's contacts, relationship graph, access control (trusted contacts
10
10
  ## Prerequisites
11
11
 
12
12
  - Use the injected `INTERNAL_GATEWAY_BASE_URL` for gateway API calls.
13
- - Use gateway control-plane routes only: this skill calls `/v1/contacts`, `/v1/ingress/*`, and `/v1/integrations/telegram/config` on the gateway, never the assistant runtime port directly.
13
+ - Use gateway control-plane routes only: this skill calls `/v1/contacts`, `/v1/contacts/channels`, `/v1/contacts/invites`, and `/v1/integrations/telegram/config` on the gateway, never the assistant runtime port directly.
14
14
  - The bearer token is available as the `$GATEWAY_AUTH_TOKEN` environment variable for control-plane `curl` requests.
15
15
 
16
16
  ## Contact Management
@@ -25,10 +25,7 @@ curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts" \
25
25
  -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
26
26
  -d '{
27
27
  "displayName": "<name>",
28
- "relationship": "<relationship>",
29
- "importance": 0.5,
30
- "responseExpectation": "<speed>",
31
- "preferredTone": "<tone>",
28
+ "notes": "<free-text notes about this contact>",
32
29
  "channels": [
33
30
  {
34
31
  "type": "<channel_type>",
@@ -48,15 +45,12 @@ Required fields:
48
45
  Optional fields:
49
46
 
50
47
  - `id` -- contact ID to update (omit to create new, or auto-match by channel address)
51
- - `relationship` -- e.g. colleague, friend, manager, client, family
52
- - `importance` -- score from 0 to 1 (default 0.5), higher means more important
53
- - `responseExpectation` -- expected response speed: immediate, within_hours, within_day, casual
54
- - `preferredTone` -- communication tone: formal, casual, friendly, professional
48
+ - `notes` -- free-text notes about this contact (e.g. relationship, communication preferences, response expectations)
55
49
  - `channels` -- list of communication channels
56
50
 
57
51
  ### Search contacts
58
52
 
59
- Search for contacts by name, channel address, relationship type, or other criteria using the gateway API.
53
+ Search for contacts by name, channel address, or other criteria using the gateway API.
60
54
 
61
55
  ```bash
62
56
  curl -s "$INTERNAL_GATEWAY_BASE_URL/v1/contacts?query=<search_term>" \
@@ -68,7 +62,6 @@ Optional query parameters:
68
62
  - `query` -- search by display name (partial match)
69
63
  - `channelAddress` -- search by channel address (email, phone, handle)
70
64
  - `channelType` -- filter by channel type when searching by address
71
- - `relationship` -- filter by relationship type (exact match)
72
65
  - `limit` -- maximum results to return (default 50, max 100)
73
66
 
74
67
  ### Merge contacts
@@ -76,7 +69,7 @@ Optional query parameters:
76
69
  When you discover two contacts are the same person (e.g. same person on email and Slack), merge them to consolidate. Merging:
77
70
 
78
71
  - Combines all channels from both contacts
79
- - Keeps the higher importance score
72
+ - Merges notes from both contacts
80
73
  - Sums interaction counts
81
74
  - Deletes the donor contact
82
75
 
@@ -96,10 +89,10 @@ Trusted contacts control who is allowed to send messages to the assistant throug
96
89
 
97
90
  ### Concepts
98
91
 
99
- - **Member**: A user identity (external user ID or chat ID) from a specific channel that has been registered with a policy.
100
- - **Policy**: Controls what the member can do -- `allow` (can message freely) or `deny` (blocked from messaging).
101
- - **Status**: The member's lifecycle state -- `active` (currently effective), `revoked` (access removed), or `blocked` (explicitly denied).
102
- - **Source channel**: The messaging platform the contact uses (e.g., `telegram`, `sms`, `voice`).
92
+ - **Contact channel**: A user identity (external user ID or chat ID) on a specific messaging platform, stored as an entry in a contact's `channels` array. Each channel entry has its own `status` and `policy`.
93
+ - **Policy**: Controls what the contact channel can do -- `allow` (can message freely) or `deny` (blocked from messaging).
94
+ - **Status**: The channel's lifecycle state -- `active` (currently effective), `revoked` (access removed), or `blocked` (explicitly denied).
95
+ - **Channel type**: The messaging platform (e.g., `telegram`, `sms`, `voice`).
103
96
 
104
97
  ### List trusted contacts
105
98
 
@@ -146,27 +139,30 @@ Use this when the user wants to grant someone access to message the assistant. *
146
139
  Ask the user: _"I'll add [name/identifier] on [channel] as an allowed contact. Should I proceed?"_
147
140
 
148
141
  ```bash
149
- curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/members" \
142
+ curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts" \
150
143
  -H "Content-Type: application/json" \
151
144
  -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
152
145
  -d '{
153
- "sourceChannel": "<channel>",
154
- "externalUserId": "<user_id>",
155
146
  "displayName": "<display_name>",
156
- "policy": "allow",
157
- "status": "active"
147
+ "channels": [{
148
+ "type": "<channel>",
149
+ "address": "<user_id>",
150
+ "externalUserId": "<user_id>",
151
+ "status": "active",
152
+ "policy": "allow"
153
+ }]
158
154
  }'
159
155
  ```
160
156
 
161
157
  Required fields:
162
158
 
163
- - `sourceChannel` -- the channel (e.g., `telegram`, `sms`)
164
- - At least one of `externalUserId` or `externalChatId`
165
-
166
- Optional fields:
167
-
168
159
  - `displayName` -- human-readable name for the contact
169
- - `username` -- channel-specific handle (e.g., Telegram @username)
160
+ - `channels` -- at least one channel entry with:
161
+ - `type` -- the channel type (e.g., `telegram`, `sms`)
162
+ - `address` -- the channel-specific identifier
163
+ - `externalUserId` -- the user's ID on that channel (or `externalChatId` for chat-based channels)
164
+ - `status` -- set to `"active"` for immediate access
165
+ - `policy` -- set to `"allow"` to grant messaging access
170
166
 
171
167
  If the user provides a name but not an external ID, explain that you need the channel-specific user ID or chat ID to create the contact entry. For Telegram, this is a numeric user ID; for SMS, this is the phone number in E.164 format.
172
168
 
@@ -176,16 +172,18 @@ Use this when the user wants to remove someone's access. **Always confirm with t
176
172
 
177
173
  Ask the user: _"I'll revoke access for [name/identifier]. They will no longer be able to message the assistant. Should I proceed?"_
178
174
 
179
- First, list members to find the member's `id`, then revoke:
175
+ First, list contacts to find the channel's `id` (each entry in a contact's `channels` array has an `id` field -- visible in `GET /v1/contacts` or `vellum contacts list --json` output), then revoke:
176
+
177
+ **Important**: Before revoking, check the channel's current `status`. If the channel is **blocked**, do not attempt to revoke it -- blocking is stronger than revoking. Inform the user that the contact is already blocked and revoking is not applicable. Only channels with `active` or `pending` status can be revoked.
180
178
 
181
179
  ```bash
182
- curl -s -X DELETE "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/members/<member_id>" \
183
- -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
180
+ curl -s -X PATCH "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/channels/<channel_id>" \
184
181
  -H "Content-Type: application/json" \
185
- -d '{"reason": "<optional reason>"}'
182
+ -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
183
+ -d '{"status": "revoked", "reason": "<optional reason>"}'
186
184
  ```
187
185
 
188
- Replace `<member_id>` with the member's `id` from the list response.
186
+ Replace `<channel_id>` with the channel's `id` from the contact's `channels` array. The API will return a `409 Conflict` error if the channel is currently blocked.
189
187
 
190
188
  ### Block a user
191
189
 
@@ -194,12 +192,14 @@ Use this when the user wants to explicitly block someone. Blocking is stronger t
194
192
  Ask the user: _"I'll block [name/identifier]. They will be permanently denied from messaging the assistant. Should I proceed?"_
195
193
 
196
194
  ```bash
197
- curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/members/<member_id>/block" \
195
+ curl -s -X PATCH "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/channels/<channel_id>" \
198
196
  -H "Content-Type: application/json" \
199
197
  -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
200
- -d '{"reason": "<optional reason>"}'
198
+ -d '{"status": "blocked", "reason": "<optional reason>"}'
201
199
  ```
202
200
 
201
+ Replace `<channel_id>` with the channel's `id` from the contact's `channels` array (visible in `GET /v1/contacts` or `vellum contacts list --json` output).
202
+
203
203
  ## Invite Links
204
204
 
205
205
  Invite links let the guardian share a link or code that automatically grants access when used. Telegram invites use a deep link; voice invites use a phone number + numeric code.
@@ -211,7 +211,7 @@ Use this when the guardian wants to invite someone to message the assistant on T
211
211
  **Important**: The shell snippet below emits a `<vellum-sensitive-output>` directive containing the raw invite token. The tool executor automatically strips this directive and replaces the raw token with a placeholder so the LLM never sees it. The placeholder is resolved back to the real token in the final assistant reply.
212
212
 
213
213
  ```bash
214
- INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites" \
214
+ INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/invites" \
215
215
  -H "Content-Type: application/json" \
216
216
  -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
217
217
  -d '{
@@ -286,7 +286,7 @@ Use this when the guardian wants to authorize a specific phone number to call th
286
286
  **Important**: The response includes a `voiceCode` field that is only returned at creation time and cannot be retrieved later. Extract and present it clearly.
287
287
 
288
288
  ```bash
289
- INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites" \
289
+ INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/invites" \
290
290
  -H "Content-Type: application/json" \
291
291
  -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
292
292
  -d '{
@@ -388,7 +388,7 @@ Ask the user: _"I'll revoke the invite [note or ID]. It will no longer be usable
388
388
  First, list invites to find the invite's `id`, then revoke:
389
389
 
390
390
  ```bash
391
- curl -s -X DELETE "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites/<invite_id>" \
391
+ curl -s -X DELETE "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/invites/<invite_id>" \
392
392
  -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
393
393
  ```
394
394
 
@@ -397,10 +397,7 @@ Replace `<invite_id>` with the invite's `id` from the list response. The same re
397
397
  ## Contact Fields
398
398
 
399
399
  - **displayName** -- the contact's name (required)
400
- - **relationship** -- e.g. colleague, friend, manager, client, family
401
- - **importance** -- score from 0 to 1 (default 0.5), higher means more important
402
- - **responseExpectation** -- expected response speed: immediate, within_hours, within_day, casual
403
- - **preferredTone** -- communication tone: formal, casual, friendly, professional
400
+ - **notes** -- free-text notes about this contact (e.g. relationship, communication preferences, response expectations)
404
401
  - **channels** -- list of communication channels (email, slack, whatsapp, phone, telegram, discord, other)
405
402
 
406
403
  ### Channel Types
@@ -425,10 +422,10 @@ Each channel has:
425
422
 
426
423
  - If a request returns `{ ok: false, error: "..." }`, report the error message to the user.
427
424
  - Common errors:
428
- - `sourceChannel is required` -- ask the user which channel the contact is on.
429
- - `At least one of externalUserId or externalChatId is required` -- ask the user for the contact's channel-specific identifier.
430
- - `Member not found or cannot be revoked` -- the member ID may be invalid or the member is already revoked.
431
- - `Member not found or already blocked` -- the member ID may be invalid or the member is already blocked.
425
+ - `Channel not found` -- the channel ID may be invalid; list contacts to find the correct channel ID.
426
+ - `Channel already revoked` -- the channel has already been revoked.
427
+ - `Channel already blocked` -- the channel has already been blocked.
428
+ - `Cannot revoke a blocked channel` -- the channel is blocked; blocking is stronger than revoking. Tell the user the contact is already blocked.
432
429
  - `sourceChannel is required for create` -- when creating an invite, always pass `"sourceChannel": "telegram"` for Telegram or `"sourceChannel": "voice"` for voice invites.
433
430
  - `expectedExternalUserId is required for voice invites` -- voice invites must include the invitee's phone number.
434
431
  - `expectedExternalUserId must be in E.164 format` -- the phone number must start with `+` followed by country code and number (e.g., `+15551234567`).
@@ -439,21 +436,20 @@ Each channel has:
439
436
  ## Tips
440
437
 
441
438
  - Use contact search with `channelAddress` to find contacts by their email, phone, or handle.
442
- - When creating follow-ups, provide a `contact_id` to link the follow-up to a specific contact for grace period calculations.
443
- - Contacts with higher importance scores get shorter default response deadlines.
444
- - When merging contacts, the surviving contact keeps the higher importance score and gains all channels from the donor.
439
+ - When creating follow-ups, provide a `contact_id` to link the follow-up to a specific contact.
440
+ - When merging contacts, the surviving contact gains all channels and merged notes from the donor.
445
441
 
446
442
  ## Typical Workflows
447
443
 
448
- **"Who can message me?"** -- List all active members, present as a formatted list.
444
+ **"Who can message me?"** -- List all contacts, present active channels as a formatted list.
449
445
 
450
- **"Add my friend to Telegram"** -- Ask for their Telegram user ID (numeric) and optional display name, confirm, then add with `policy: "allow"` and `status: "active"`.
446
+ **"Add my friend to Telegram"** -- Ask for their Telegram user ID (numeric) and display name, confirm, then create a contact with a channel entry with `policy: "allow"` and `status: "active"`.
451
447
 
452
- **"Remove [name]'s access"** -- List members to find them, confirm the revocation, then delete.
448
+ **"Remove [name]'s access"** -- List contacts to find them, identify the channel to revoke, confirm the revocation, then patch the channel status to `"revoked"`.
453
449
 
454
- **"Block [name]"** -- List members to find them, confirm the block, then execute.
450
+ **"Block [name]"** -- List contacts to find them, identify the channel to block, confirm the block, then patch the channel status to `"blocked"`.
455
451
 
456
- **"Show me blocked contacts"** -- List with `status=blocked` filter.
452
+ **"Show me blocked contacts"** -- List contacts and filter for channels with `status: "blocked"`.
457
453
 
458
454
  **"Create a Telegram invite link"** / **"Invite someone on Telegram"** -- Create an invite with `sourceChannel: "telegram"`, look up the bot username, build the deep link, and present it with sharing instructions.
459
455
 
@@ -17,21 +17,9 @@
17
17
  "type": "string",
18
18
  "description": "Display name for the contact"
19
19
  },
20
- "relationship": {
20
+ "notes": {
21
21
  "type": "string",
22
- "description": "Relationship type (e.g. colleague, friend, manager, client, family)"
23
- },
24
- "importance": {
25
- "type": "number",
26
- "description": "Importance score 0-1 (default 0.5). Higher = more important."
27
- },
28
- "response_expectation": {
29
- "type": "string",
30
- "description": "Expected response speed (e.g. immediate, within_hours, within_day, casual)"
31
- },
32
- "preferred_tone": {
33
- "type": "string",
34
- "description": "Preferred communication tone (e.g. formal, casual, friendly, professional)"
22
+ "description": "Free-text notes about this contact (e.g. relationship, communication preferences, response expectations)"
35
23
  },
36
24
  "channels": {
37
25
  "type": "array",
@@ -41,7 +29,15 @@
41
29
  "properties": {
42
30
  "type": {
43
31
  "type": "string",
44
- "enum": ["email", "slack", "whatsapp", "phone", "telegram", "discord", "other"],
32
+ "enum": [
33
+ "email",
34
+ "slack",
35
+ "whatsapp",
36
+ "phone",
37
+ "telegram",
38
+ "discord",
39
+ "other"
40
+ ],
45
41
  "description": "Channel type"
46
42
  },
47
43
  "address": {
@@ -55,6 +51,10 @@
55
51
  },
56
52
  "required": ["type", "address"]
57
53
  }
54
+ },
55
+ "reason": {
56
+ "type": "string",
57
+ "description": "Brief non-technical explanation of why this tool is being called"
58
58
  }
59
59
  },
60
60
  "required": ["display_name"]
@@ -64,7 +64,7 @@
64
64
  },
65
65
  {
66
66
  "name": "contact_search",
67
- "description": "Search for contacts by name, channel address, relationship type, or other criteria. Returns matching contacts with their channel information.",
67
+ "description": "Search for contacts by name, channel address, or other criteria. Returns matching contacts with their channel information.",
68
68
  "category": "contacts",
69
69
  "risk": "low",
70
70
  "input_schema": {
@@ -76,19 +76,19 @@
76
76
  },
77
77
  "channel_address": {
78
78
  "type": "string",
79
- "description": "Search by channel address (email, phone, handle partial match)"
79
+ "description": "Search by channel address (email, phone, handle \u2014 partial match)"
80
80
  },
81
81
  "channel_type": {
82
82
  "type": "string",
83
83
  "description": "Filter by channel type when searching by address (email, slack, whatsapp, phone, etc.)"
84
84
  },
85
- "relationship": {
86
- "type": "string",
87
- "description": "Filter by relationship type (exact match)"
88
- },
89
85
  "limit": {
90
86
  "type": "number",
91
87
  "description": "Maximum results to return (default 20, max 100)"
88
+ },
89
+ "reason": {
90
+ "type": "string",
91
+ "description": "Brief non-technical explanation of why this tool is being called"
92
92
  }
93
93
  },
94
94
  "required": []
@@ -98,7 +98,7 @@
98
98
  },
99
99
  {
100
100
  "name": "contact_merge",
101
- "description": "Merge two contacts when you discover they are the same person (e.g. same person on email and Slack). Combines channels, keeps the higher importance, and deletes the donor contact.",
101
+ "description": "Merge two contacts when you discover they are the same person (e.g. same person on email and Slack). Combines channels, merges notes, and deletes the donor contact.",
102
102
  "category": "contacts",
103
103
  "risk": "medium",
104
104
  "input_schema": {
@@ -111,6 +111,10 @@
111
111
  "merge_id": {
112
112
  "type": "string",
113
113
  "description": "ID of the contact to merge into the kept contact (will be deleted)"
114
+ },
115
+ "reason": {
116
+ "type": "string",
117
+ "description": "Brief non-technical explanation of why this tool is being called"
114
118
  }
115
119
  },
116
120
  "required": ["keep_id", "merge_id"]
@@ -1,5 +1,8 @@
1
- import { getGatewayInternalBaseUrl } from "../../../../config/env.js";
2
- import { mintEdgeRelayToken } from "../../../../runtime/auth/token-service.js";
1
+ import {
2
+ gatewayGet,
3
+ gatewayPost,
4
+ GatewayRequestError,
5
+ } from "../../../../runtime/gateway-internal-client.js";
3
6
  import type {
4
7
  ToolContext,
5
8
  ToolExecutionResult,
@@ -14,8 +17,7 @@ interface ContactChannel {
14
17
  interface ContactResponse {
15
18
  id: string;
16
19
  displayName: string;
17
- relationship: string | null;
18
- importance: number;
20
+ notes: string | null;
19
21
  interactionCount: number;
20
22
  channels: ContactChannel[];
21
23
  }
@@ -35,72 +37,49 @@ export async function executeContactMerge(
35
37
  }
36
38
 
37
39
  try {
38
- const gatewayBase = getGatewayInternalBaseUrl();
39
- const token = mintEdgeRelayToken();
40
- const headers = {
41
- Accept: "application/json",
42
- Authorization: `Bearer ${token}`,
43
- };
44
-
45
40
  // Validate both contacts exist before merging
46
- const [keepResp, mergeResp] = await Promise.all([
47
- fetch(`${gatewayBase}/v1/contacts/${keepId}`, {
48
- method: "GET",
49
- headers,
50
- }),
51
- fetch(`${gatewayBase}/v1/contacts/${mergeId}`, {
52
- method: "GET",
53
- headers,
54
- }),
41
+ const [keepResult, mergeResult] = await Promise.allSettled([
42
+ gatewayGet<{ ok: boolean; contact: ContactResponse }>(
43
+ `/v1/contacts/${keepId}`,
44
+ ),
45
+ gatewayGet<{ ok: boolean; contact: ContactResponse }>(
46
+ `/v1/contacts/${mergeId}`,
47
+ ),
55
48
  ]);
56
49
 
57
- if (!keepResp.ok) {
58
- return { content: `Error: Contact "${keepId}" not found`, isError: true };
50
+ if (keepResult.status === "rejected") {
51
+ if (
52
+ keepResult.reason instanceof GatewayRequestError &&
53
+ keepResult.reason.statusCode === 404
54
+ ) {
55
+ return {
56
+ content: `Error: Contact "${keepId}" not found`,
57
+ isError: true,
58
+ };
59
+ }
60
+ throw keepResult.reason;
59
61
  }
60
- if (!mergeResp.ok) {
61
- return {
62
- content: `Error: Contact "${mergeId}" not found`,
63
- isError: true,
64
- };
62
+ if (mergeResult.status === "rejected") {
63
+ if (
64
+ mergeResult.reason instanceof GatewayRequestError &&
65
+ mergeResult.reason.statusCode === 404
66
+ ) {
67
+ return {
68
+ content: `Error: Contact "${mergeId}" not found`,
69
+ isError: true,
70
+ };
71
+ }
72
+ throw mergeResult.reason;
65
73
  }
66
74
 
67
- const keepData = (await keepResp.json()) as {
68
- ok: boolean;
69
- contact: ContactResponse;
70
- };
71
- const mergeData = (await mergeResp.json()) as {
72
- ok: boolean;
73
- contact: ContactResponse;
74
- };
75
- const keepContact = keepData.contact;
76
- const mergeContact = mergeData.contact;
75
+ const keepContact = keepResult.value.contact;
76
+ const mergeContact = mergeResult.value.contact;
77
77
 
78
78
  // Execute the merge
79
- const mergeResult = await fetch(`${gatewayBase}/v1/contacts/merge`, {
80
- method: "POST",
81
- headers: {
82
- ...headers,
83
- "Content-Type": "application/json",
84
- },
85
- body: JSON.stringify({ keepId, mergeId }),
86
- });
87
-
88
- if (!mergeResult.ok) {
89
- const body = await mergeResult.text();
90
- let message = `Gateway request failed (${mergeResult.status})`;
91
- try {
92
- const parsed = JSON.parse(body) as { error?: string };
93
- if (parsed.error) message = parsed.error;
94
- } catch {
95
- if (body) message = body;
96
- }
97
- return { content: `Error: ${message}`, isError: true };
98
- }
99
-
100
- const resultData = (await mergeResult.json()) as {
79
+ const { data: resultData } = await gatewayPost<{
101
80
  ok: boolean;
102
81
  contact: ContactResponse;
103
- };
82
+ }>("/v1/contacts/merge", { keepId, mergeId });
104
83
  const merged = resultData.contact;
105
84
 
106
85
  const channelList = merged.channels
@@ -116,9 +95,8 @@ export async function executeContactMerge(
116
95
  ``,
117
96
  `Surviving contact (${merged.id}):`,
118
97
  ` Name: ${merged.displayName}`,
119
- ` Importance: ${merged.importance.toFixed(2)}`,
120
98
  ` Interactions: ${merged.interactionCount}`,
121
- merged.relationship ? ` Relationship: ${merged.relationship}` : null,
99
+ merged.notes ? ` Notes: ${merged.notes}` : null,
122
100
  merged.channels.length > 0 ? ` Channels:\n${channelList}` : null,
123
101
  ``,
124
102
  `Deleted contact: ${mergeContact.displayName} (${mergeId})`,
@@ -1,5 +1,7 @@
1
- import { getGatewayInternalBaseUrl } from "../../../../config/env.js";
2
- import { mintEdgeRelayToken } from "../../../../runtime/auth/token-service.js";
1
+ import {
2
+ gatewayGet,
3
+ GatewayRequestError,
4
+ } from "../../../../runtime/gateway-internal-client.js";
3
5
  import type {
4
6
  ToolContext,
5
7
  ToolExecutionResult,
@@ -14,20 +16,16 @@ interface ContactChannel {
14
16
  interface ContactResponse {
15
17
  id: string;
16
18
  displayName: string;
17
- relationship: string | null;
18
- importance: number;
19
+ notes: string | null;
19
20
  interactionCount: number;
20
21
  channels: ContactChannel[];
21
22
  }
22
23
 
23
24
  function formatContactSummary(c: ContactResponse): string {
24
25
  const parts = [`- **${c.displayName}** (ID: ${c.id})`];
25
- if (c.relationship) parts.push(` Relationship: ${c.relationship}`);
26
- parts.push(
27
- ` Importance: ${c.importance.toFixed(2)} | Interactions: ${
28
- c.interactionCount
29
- }`,
30
- );
26
+ if (c.notes) parts.push(` Notes: ${c.notes}`);
27
+ if (c.interactionCount > 0)
28
+ parts.push(` Interactions: ${c.interactionCount}`);
31
29
  if (c.channels.length > 0) {
32
30
  const channelList = c.channels
33
31
  .map((ch) => `${ch.type}:${ch.address}${ch.isPrimary ? "*" : ""}`)
@@ -44,55 +42,27 @@ export async function executeContactSearch(
44
42
  const query = input.query as string | undefined;
45
43
  const channelAddress = input.channel_address as string | undefined;
46
44
  const channelType = input.channel_type as string | undefined;
47
- const relationship = input.relationship as string | undefined;
48
45
  const limit = input.limit as number | undefined;
49
46
 
50
- if (!query && !channelAddress && !relationship) {
47
+ if (!query && !channelAddress) {
51
48
  return {
52
49
  content:
53
- "Error: At least one search criterion is required (query, channel_address, or relationship)",
50
+ "Error: At least one search criterion is required (query or channel_address)",
54
51
  isError: true,
55
52
  };
56
53
  }
57
54
 
58
55
  try {
59
- const gatewayBase = getGatewayInternalBaseUrl();
60
- const token = mintEdgeRelayToken();
61
-
62
56
  const params = new URLSearchParams();
63
57
  if (query) params.set("query", query);
64
58
  if (channelAddress) params.set("channelAddress", channelAddress);
65
59
  if (channelType) params.set("channelType", channelType);
66
- if (relationship) params.set("relationship", relationship);
67
60
  if (limit !== undefined) params.set("limit", String(limit));
68
61
 
69
62
  const qs = params.toString();
70
- const url = `${gatewayBase}/v1/contacts${qs ? `?${qs}` : ""}`;
71
-
72
- const resp = await fetch(url, {
73
- method: "GET",
74
- headers: {
75
- Accept: "application/json",
76
- Authorization: `Bearer ${token}`,
77
- },
78
- });
79
-
80
- if (!resp.ok) {
81
- const body = await resp.text();
82
- let message = `Gateway request failed (${resp.status})`;
83
- try {
84
- const parsed = JSON.parse(body) as { error?: string };
85
- if (parsed.error) message = parsed.error;
86
- } catch {
87
- if (body) message = body;
88
- }
89
- return { content: `Error: ${message}`, isError: true };
90
- }
91
-
92
- const data = (await resp.json()) as {
93
- ok: boolean;
94
- contacts: ContactResponse[];
95
- };
63
+ const data = await gatewayGet<{ ok: boolean; contacts: ContactResponse[] }>(
64
+ `/v1/contacts${qs ? `?${qs}` : ""}`,
65
+ );
96
66
  const results = data.contacts;
97
67
 
98
68
  if (results.length === 0) {
@@ -109,6 +79,10 @@ export async function executeContactSearch(
109
79
 
110
80
  return { content: lines.join("\n"), isError: false };
111
81
  } catch (err) {
82
+ if (err instanceof GatewayRequestError) {
83
+ const message = err.gatewayError ?? err.message;
84
+ return { content: `Error: ${message}`, isError: true };
85
+ }
112
86
  const msg = err instanceof Error ? err.message : String(err);
113
87
  return { content: `Error: ${msg}`, isError: true };
114
88
  }