@vellumai/assistant 0.4.29 → 0.4.30

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 (174) hide show
  1. package/ARCHITECTURE.md +39 -37
  2. package/README.md +5 -6
  3. package/docs/runbook-trusted-contacts.md +79 -43
  4. package/package.json +1 -1
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
  6. package/scripts/test.sh +1 -1
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
  8. package/src/__tests__/actor-token-service.test.ts +4 -3
  9. package/src/__tests__/app-executors.test.ts +7 -17
  10. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
  11. package/src/__tests__/browser-skill-endstate.test.ts +10 -1
  12. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
  13. package/src/__tests__/channel-approval-routes.test.ts +44 -44
  14. package/src/__tests__/channel-approval.test.ts +8 -0
  15. package/src/__tests__/channel-approvals.test.ts +39 -1
  16. package/src/__tests__/channel-guardian.test.ts +15 -5
  17. package/src/__tests__/channel-reply-delivery.test.ts +31 -0
  18. package/src/__tests__/commit-message-enrichment-service.test.ts +4 -0
  19. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
  20. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  21. package/src/__tests__/gemini-image-service.test.ts +2 -2
  22. package/src/__tests__/guardian-grant-minting.test.ts +6 -6
  23. package/src/__tests__/guardian-routing-invariants.test.ts +34 -11
  24. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
  25. package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
  26. package/src/__tests__/integrations-cli.test.ts +3 -27
  27. package/src/__tests__/intent-routing.test.ts +3 -0
  28. package/src/__tests__/invite-redemption-service.test.ts +1 -1
  29. package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
  30. package/src/__tests__/ipc-snapshot.test.ts +4 -31
  31. package/src/__tests__/nl-approval-parser.test.ts +305 -0
  32. package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
  33. package/src/__tests__/provider-error-scenarios.test.ts +68 -0
  34. package/src/__tests__/relay-server.test.ts +1 -1
  35. package/src/__tests__/retry-after-extraction.test.ts +111 -0
  36. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
  37. package/src/__tests__/session-media-retry.test.ts +147 -0
  38. package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
  39. package/src/__tests__/skill-feature-flags.test.ts +18 -12
  40. package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
  41. package/src/__tests__/slack-block-formatting.test.ts +100 -0
  42. package/src/__tests__/slack-inbound-verification.test.ts +346 -0
  43. package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
  44. package/src/__tests__/slack-skill.test.ts +3 -2
  45. package/src/__tests__/starter-task-flow.test.ts +0 -1
  46. package/src/__tests__/trusted-contact-verification.test.ts +3 -1
  47. package/src/__tests__/voice-invite-redemption.test.ts +1 -1
  48. package/src/amazon/client.ts +7 -24
  49. package/src/calls/relay-server.ts +39 -11
  50. package/src/channels/config.ts +1 -1
  51. package/src/cli/integrations.ts +10 -66
  52. package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
  53. package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
  54. package/src/config/bundled-skills/browser/TOOLS.json +59 -2
  55. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
  56. package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
  57. package/src/config/bundled-skills/contacts/SKILL.md +42 -35
  58. package/src/config/bundled-skills/contacts/TOOLS.json +22 -2
  59. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +38 -58
  60. package/src/config/bundled-skills/contacts/tools/contact-search.ts +11 -31
  61. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +19 -37
  62. package/src/config/bundled-skills/document/TOOLS.json +8 -0
  63. package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
  64. package/src/config/bundled-skills/followups/TOOLS.json +12 -0
  65. package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
  66. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
  67. package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
  68. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
  69. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
  70. package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
  71. package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
  72. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
  73. package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
  74. package/src/config/bundled-skills/notifications/SKILL.md +3 -2
  75. package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
  76. package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
  77. package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
  78. package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
  79. package/src/config/bundled-skills/schedule/SKILL.md +33 -15
  80. package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
  81. package/src/config/bundled-skills/slack/SKILL.md +30 -1
  82. package/src/config/bundled-skills/slack/TOOLS.json +89 -2
  83. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
  84. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
  85. package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
  86. package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
  87. package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
  88. package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
  89. package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
  90. package/src/config/bundled-skills/weather/TOOLS.json +4 -0
  91. package/src/config/bundled-tool-registry.ts +2 -0
  92. package/src/config/channel-permission-profiles.ts +155 -0
  93. package/src/config/env.ts +4 -1
  94. package/src/contacts/contact-store.ts +195 -4
  95. package/src/contacts/types.ts +26 -0
  96. package/src/daemon/assistant-attachments.ts +23 -3
  97. package/src/daemon/guardian-verification-intent.ts +7 -4
  98. package/src/daemon/handlers/apps.ts +1 -2
  99. package/src/daemon/handlers/config-inbox.ts +16 -134
  100. package/src/daemon/handlers/guardian-actions.ts +20 -87
  101. package/src/daemon/handlers/sessions.ts +0 -1
  102. package/src/daemon/ipc-contract/apps.ts +0 -1
  103. package/src/daemon/ipc-contract/inbox.ts +7 -66
  104. package/src/daemon/ipc-contract/sessions.ts +1 -0
  105. package/src/daemon/ipc-contract/surfaces.ts +0 -1
  106. package/src/daemon/ipc-contract-inventory.json +2 -4
  107. package/src/daemon/lifecycle.ts +14 -2
  108. package/src/daemon/session-agent-loop-handlers.ts +9 -0
  109. package/src/daemon/session-agent-loop.ts +1 -0
  110. package/src/daemon/session-attachments.ts +5 -1
  111. package/src/daemon/session-error.ts +18 -0
  112. package/src/daemon/session-lifecycle.ts +4 -5
  113. package/src/daemon/session-media-retry.ts +15 -1
  114. package/src/daemon/session-surfaces.ts +0 -1
  115. package/src/daemon/session-tool-setup.ts +7 -4
  116. package/src/events/domain-events.ts +2 -1
  117. package/src/home-base/prebuilt/seed.ts +0 -1
  118. package/src/influencer/client.ts +7 -24
  119. package/src/media/gemini-image-service.ts +48 -3
  120. package/src/memory/app-store.ts +0 -4
  121. package/src/memory/conversation-attention-store.ts +3 -1
  122. package/src/memory/db-init.ts +4 -0
  123. package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
  124. package/src/memory/migrations/index.ts +1 -0
  125. package/src/memory/schema.ts +12 -0
  126. package/src/memory/slack-thread-store.ts +187 -0
  127. package/src/messaging/providers/slack/client.ts +84 -26
  128. package/src/messaging/providers/slack/types.ts +4 -0
  129. package/src/notifications/adapters/slack.ts +90 -0
  130. package/src/notifications/destination-resolver.ts +42 -1
  131. package/src/notifications/emit-signal.ts +17 -1
  132. package/src/oauth/provider-profiles.ts +22 -0
  133. package/src/providers/anthropic/client.ts +3 -0
  134. package/src/providers/openai/client.ts +3 -0
  135. package/src/providers/retry.ts +9 -1
  136. package/src/runtime/actor-trust-resolver.ts +8 -0
  137. package/src/runtime/auth/require-bound-guardian.ts +44 -0
  138. package/src/runtime/auth/route-policy.ts +4 -8
  139. package/src/runtime/channel-approval-types.ts +18 -0
  140. package/src/runtime/channel-approvals.ts +8 -0
  141. package/src/runtime/channel-invite-transport.ts +1 -1
  142. package/src/runtime/channel-reply-delivery.ts +62 -3
  143. package/src/runtime/gateway-client.ts +36 -2
  144. package/src/runtime/gateway-internal-client.ts +86 -0
  145. package/src/runtime/guardian-action-service.ts +127 -0
  146. package/src/runtime/guardian-verification-templates.ts +16 -1
  147. package/src/runtime/http-server.ts +20 -49
  148. package/src/runtime/invite-redemption-service.ts +1 -1
  149. package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
  150. package/src/runtime/nl-approval-parser.ts +138 -0
  151. package/src/runtime/routes/approval-routes.ts +1 -40
  152. package/src/runtime/routes/channel-route-shared.ts +35 -1
  153. package/src/runtime/routes/contact-routes.ts +196 -28
  154. package/src/runtime/routes/guardian-action-routes.ts +19 -111
  155. package/src/runtime/routes/guardian-approval-interception.ts +76 -0
  156. package/src/runtime/routes/inbound-message-handler.ts +40 -12
  157. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +222 -0
  158. package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
  159. package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
  160. package/src/runtime/slack-block-formatting.ts +176 -0
  161. package/src/schedule/scheduler.ts +11 -2
  162. package/src/tools/apps/executors.ts +16 -15
  163. package/src/tools/calls/call-end.ts +1 -1
  164. package/src/tools/computer-use/definitions.ts +16 -0
  165. package/src/tools/credentials/vault.ts +86 -2
  166. package/src/tools/network/script-proxy/session-manager.ts +28 -3
  167. package/src/tools/permission-checker.ts +18 -0
  168. package/src/tools/terminal/shell.ts +15 -5
  169. package/src/tools/tool-approval-handler.ts +48 -4
  170. package/src/tools/types.ts +38 -1
  171. package/src/util/errors.ts +5 -1
  172. package/src/util/retry.ts +21 -0
  173. package/src/watcher/providers/slack.ts +33 -3
  174. /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
@@ -96,10 +96,10 @@ Trusted contacts control who is allowed to send messages to the assistant throug
96
96
 
97
97
  ### Concepts
98
98
 
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`).
99
+ - **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`.
100
+ - **Policy**: Controls what the contact channel can do -- `allow` (can message freely) or `deny` (blocked from messaging).
101
+ - **Status**: The channel's lifecycle state -- `active` (currently effective), `revoked` (access removed), or `blocked` (explicitly denied).
102
+ - **Channel type**: The messaging platform (e.g., `telegram`, `sms`, `voice`).
103
103
 
104
104
  ### List trusted contacts
105
105
 
@@ -146,27 +146,30 @@ Use this when the user wants to grant someone access to message the assistant. *
146
146
  Ask the user: _"I'll add [name/identifier] on [channel] as an allowed contact. Should I proceed?"_
147
147
 
148
148
  ```bash
149
- curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/members" \
149
+ curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts" \
150
150
  -H "Content-Type: application/json" \
151
151
  -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
152
152
  -d '{
153
- "sourceChannel": "<channel>",
154
- "externalUserId": "<user_id>",
155
153
  "displayName": "<display_name>",
156
- "policy": "allow",
157
- "status": "active"
154
+ "channels": [{
155
+ "type": "<channel>",
156
+ "address": "<user_id>",
157
+ "externalUserId": "<user_id>",
158
+ "status": "active",
159
+ "policy": "allow"
160
+ }]
158
161
  }'
159
162
  ```
160
163
 
161
164
  Required fields:
162
165
 
163
- - `sourceChannel` -- the channel (e.g., `telegram`, `sms`)
164
- - At least one of `externalUserId` or `externalChatId`
165
-
166
- Optional fields:
167
-
168
166
  - `displayName` -- human-readable name for the contact
169
- - `username` -- channel-specific handle (e.g., Telegram @username)
167
+ - `channels` -- at least one channel entry with:
168
+ - `type` -- the channel type (e.g., `telegram`, `sms`)
169
+ - `address` -- the channel-specific identifier
170
+ - `externalUserId` -- the user's ID on that channel (or `externalChatId` for chat-based channels)
171
+ - `status` -- set to `"active"` for immediate access
172
+ - `policy` -- set to `"allow"` to grant messaging access
170
173
 
171
174
  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
175
 
@@ -176,16 +179,18 @@ Use this when the user wants to remove someone's access. **Always confirm with t
176
179
 
177
180
  Ask the user: _"I'll revoke access for [name/identifier]. They will no longer be able to message the assistant. Should I proceed?"_
178
181
 
179
- First, list members to find the member's `id`, then revoke:
182
+ 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:
183
+
184
+ **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
185
 
181
186
  ```bash
182
- curl -s -X DELETE "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/members/<member_id>" \
183
- -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
187
+ curl -s -X PATCH "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/channels/<channel_id>" \
184
188
  -H "Content-Type: application/json" \
185
- -d '{"reason": "<optional reason>"}'
189
+ -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
190
+ -d '{"status": "revoked", "reason": "<optional reason>"}'
186
191
  ```
187
192
 
188
- Replace `<member_id>` with the member's `id` from the list response.
193
+ 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
194
 
190
195
  ### Block a user
191
196
 
@@ -194,12 +199,14 @@ Use this when the user wants to explicitly block someone. Blocking is stronger t
194
199
  Ask the user: _"I'll block [name/identifier]. They will be permanently denied from messaging the assistant. Should I proceed?"_
195
200
 
196
201
  ```bash
197
- curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/members/<member_id>/block" \
202
+ curl -s -X PATCH "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/channels/<channel_id>" \
198
203
  -H "Content-Type: application/json" \
199
204
  -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
200
- -d '{"reason": "<optional reason>"}'
205
+ -d '{"status": "blocked", "reason": "<optional reason>"}'
201
206
  ```
202
207
 
208
+ 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).
209
+
203
210
  ## Invite Links
204
211
 
205
212
  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 +218,7 @@ Use this when the guardian wants to invite someone to message the assistant on T
211
218
  **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
219
 
213
220
  ```bash
214
- INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites" \
221
+ INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/invites" \
215
222
  -H "Content-Type: application/json" \
216
223
  -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
217
224
  -d '{
@@ -286,7 +293,7 @@ Use this when the guardian wants to authorize a specific phone number to call th
286
293
  **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
294
 
288
295
  ```bash
289
- INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites" \
296
+ INVITE_JSON=$(curl -s -X POST "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/invites" \
290
297
  -H "Content-Type: application/json" \
291
298
  -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN" \
292
299
  -d '{
@@ -388,7 +395,7 @@ Ask the user: _"I'll revoke the invite [note or ID]. It will no longer be usable
388
395
  First, list invites to find the invite's `id`, then revoke:
389
396
 
390
397
  ```bash
391
- curl -s -X DELETE "$INTERNAL_GATEWAY_BASE_URL/v1/ingress/invites/<invite_id>" \
398
+ curl -s -X DELETE "$INTERNAL_GATEWAY_BASE_URL/v1/contacts/invites/<invite_id>" \
392
399
  -H "Authorization: Bearer $GATEWAY_AUTH_TOKEN"
393
400
  ```
394
401
 
@@ -425,10 +432,10 @@ Each channel has:
425
432
 
426
433
  - If a request returns `{ ok: false, error: "..." }`, report the error message to the user.
427
434
  - 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.
435
+ - `Channel not found` -- the channel ID may be invalid; list contacts to find the correct channel ID.
436
+ - `Channel already revoked` -- the channel has already been revoked.
437
+ - `Channel already blocked` -- the channel has already been blocked.
438
+ - `Cannot revoke a blocked channel` -- the channel is blocked; blocking is stronger than revoking. Tell the user the contact is already blocked.
432
439
  - `sourceChannel is required for create` -- when creating an invite, always pass `"sourceChannel": "telegram"` for Telegram or `"sourceChannel": "voice"` for voice invites.
433
440
  - `expectedExternalUserId is required for voice invites` -- voice invites must include the invitee's phone number.
434
441
  - `expectedExternalUserId must be in E.164 format` -- the phone number must start with `+` followed by country code and number (e.g., `+15551234567`).
@@ -445,15 +452,15 @@ Each channel has:
445
452
 
446
453
  ## Typical Workflows
447
454
 
448
- **"Who can message me?"** -- List all active members, present as a formatted list.
455
+ **"Who can message me?"** -- List all contacts, present active channels as a formatted list.
449
456
 
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"`.
457
+ **"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
458
 
452
- **"Remove [name]'s access"** -- List members to find them, confirm the revocation, then delete.
459
+ **"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
460
 
454
- **"Block [name]"** -- List members to find them, confirm the block, then execute.
461
+ **"Block [name]"** -- List contacts to find them, identify the channel to block, confirm the block, then patch the channel status to `"blocked"`.
455
462
 
456
- **"Show me blocked contacts"** -- List with `status=blocked` filter.
463
+ **"Show me blocked contacts"** -- List contacts and filter for channels with `status: "blocked"`.
457
464
 
458
465
  **"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
466
 
@@ -41,7 +41,15 @@
41
41
  "properties": {
42
42
  "type": {
43
43
  "type": "string",
44
- "enum": ["email", "slack", "whatsapp", "phone", "telegram", "discord", "other"],
44
+ "enum": [
45
+ "email",
46
+ "slack",
47
+ "whatsapp",
48
+ "phone",
49
+ "telegram",
50
+ "discord",
51
+ "other"
52
+ ],
45
53
  "description": "Channel type"
46
54
  },
47
55
  "address": {
@@ -55,6 +63,10 @@
55
63
  },
56
64
  "required": ["type", "address"]
57
65
  }
66
+ },
67
+ "reason": {
68
+ "type": "string",
69
+ "description": "Brief non-technical explanation of why this tool is being called"
58
70
  }
59
71
  },
60
72
  "required": ["display_name"]
@@ -76,7 +88,7 @@
76
88
  },
77
89
  "channel_address": {
78
90
  "type": "string",
79
- "description": "Search by channel address (email, phone, handle partial match)"
91
+ "description": "Search by channel address (email, phone, handle \u2014 partial match)"
80
92
  },
81
93
  "channel_type": {
82
94
  "type": "string",
@@ -89,6 +101,10 @@
89
101
  "limit": {
90
102
  "type": "number",
91
103
  "description": "Maximum results to return (default 20, max 100)"
104
+ },
105
+ "reason": {
106
+ "type": "string",
107
+ "description": "Brief non-technical explanation of why this tool is being called"
92
108
  }
93
109
  },
94
110
  "required": []
@@ -111,6 +127,10 @@
111
127
  "merge_id": {
112
128
  "type": "string",
113
129
  "description": "ID of the contact to merge into the kept contact (will be deleted)"
130
+ },
131
+ "reason": {
132
+ "type": "string",
133
+ "description": "Brief non-technical explanation of why this tool is being called"
114
134
  }
115
135
  },
116
136
  "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,
@@ -35,72 +38,49 @@ export async function executeContactMerge(
35
38
  }
36
39
 
37
40
  try {
38
- const gatewayBase = getGatewayInternalBaseUrl();
39
- const token = mintEdgeRelayToken();
40
- const headers = {
41
- Accept: "application/json",
42
- Authorization: `Bearer ${token}`,
43
- };
44
-
45
41
  // 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
- }),
42
+ const [keepResult, mergeResult] = await Promise.allSettled([
43
+ gatewayGet<{ ok: boolean; contact: ContactResponse }>(
44
+ `/v1/contacts/${keepId}`,
45
+ ),
46
+ gatewayGet<{ ok: boolean; contact: ContactResponse }>(
47
+ `/v1/contacts/${mergeId}`,
48
+ ),
55
49
  ]);
56
50
 
57
- if (!keepResp.ok) {
58
- return { content: `Error: Contact "${keepId}" not found`, isError: true };
51
+ if (keepResult.status === "rejected") {
52
+ if (
53
+ keepResult.reason instanceof GatewayRequestError &&
54
+ keepResult.reason.statusCode === 404
55
+ ) {
56
+ return {
57
+ content: `Error: Contact "${keepId}" not found`,
58
+ isError: true,
59
+ };
60
+ }
61
+ throw keepResult.reason;
59
62
  }
60
- if (!mergeResp.ok) {
61
- return {
62
- content: `Error: Contact "${mergeId}" not found`,
63
- isError: true,
64
- };
63
+ if (mergeResult.status === "rejected") {
64
+ if (
65
+ mergeResult.reason instanceof GatewayRequestError &&
66
+ mergeResult.reason.statusCode === 404
67
+ ) {
68
+ return {
69
+ content: `Error: Contact "${mergeId}" not found`,
70
+ isError: true,
71
+ };
72
+ }
73
+ throw mergeResult.reason;
65
74
  }
66
75
 
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;
76
+ const keepContact = keepResult.value.contact;
77
+ const mergeContact = mergeResult.value.contact;
77
78
 
78
79
  // 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 {
80
+ const { data: resultData } = await gatewayPost<{
101
81
  ok: boolean;
102
82
  contact: ContactResponse;
103
- };
83
+ }>("/v1/contacts/merge", { keepId, mergeId });
104
84
  const merged = resultData.contact;
105
85
 
106
86
  const channelList = merged.channels
@@ -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,
@@ -56,9 +58,6 @@ export async function executeContactSearch(
56
58
  }
57
59
 
58
60
  try {
59
- const gatewayBase = getGatewayInternalBaseUrl();
60
- const token = mintEdgeRelayToken();
61
-
62
61
  const params = new URLSearchParams();
63
62
  if (query) params.set("query", query);
64
63
  if (channelAddress) params.set("channelAddress", channelAddress);
@@ -67,32 +66,9 @@ export async function executeContactSearch(
67
66
  if (limit !== undefined) params.set("limit", String(limit));
68
67
 
69
68
  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
- };
69
+ const data = await gatewayGet<{ ok: boolean; contacts: ContactResponse[] }>(
70
+ `/v1/contacts${qs ? `?${qs}` : ""}`,
71
+ );
96
72
  const results = data.contacts;
97
73
 
98
74
  if (results.length === 0) {
@@ -109,6 +85,10 @@ export async function executeContactSearch(
109
85
 
110
86
  return { content: lines.join("\n"), isError: false };
111
87
  } catch (err) {
88
+ if (err instanceof GatewayRequestError) {
89
+ const message = err.gatewayError ?? err.message;
90
+ return { content: `Error: ${message}`, isError: true };
91
+ }
112
92
  const msg = err instanceof Error ? err.message : String(err);
113
93
  return { content: `Error: ${msg}`, isError: true };
114
94
  }
@@ -1,5 +1,7 @@
1
- import { getGatewayInternalBaseUrl } from "../../../../config/env.js";
2
- import { mintEdgeRelayToken } from "../../../../runtime/auth/token-service.js";
1
+ import {
2
+ gatewayPost,
3
+ GatewayRequestError,
4
+ } from "../../../../runtime/gateway-internal-client.js";
3
5
  import type {
4
6
  ToolContext,
5
7
  ToolExecutionResult,
@@ -78,49 +80,29 @@ export async function executeContactUpsert(
78
80
  }));
79
81
 
80
82
  try {
81
- const gatewayBase = getGatewayInternalBaseUrl();
82
- const token = mintEdgeRelayToken();
83
-
84
- const resp = await fetch(`${gatewayBase}/v1/contacts`, {
85
- method: "POST",
86
- headers: {
87
- "Content-Type": "application/json",
88
- Authorization: `Bearer ${token}`,
89
- },
90
- body: JSON.stringify({
91
- id: input.id as string | undefined,
92
- displayName: displayName.trim(),
93
- relationship: input.relationship as string | undefined,
94
- importance,
95
- responseExpectation: input.response_expectation as string | undefined,
96
- preferredTone: input.preferred_tone as string | undefined,
97
- channels,
98
- }),
99
- });
100
-
101
- if (!resp.ok) {
102
- const body = await resp.text();
103
- let message = `Gateway request failed (${resp.status})`;
104
- try {
105
- const parsed = JSON.parse(body) as { error?: string };
106
- if (parsed.error) message = parsed.error;
107
- } catch {
108
- if (body) message = body;
109
- }
110
- return { content: `Error: ${message}`, isError: true };
111
- }
112
-
113
- const data = (await resp.json()) as {
83
+ const { status, data } = await gatewayPost<{
114
84
  ok: boolean;
115
85
  contact: ContactResponse;
116
- };
117
- const created = resp.status === 201;
86
+ }>("/v1/contacts", {
87
+ id: input.id as string | undefined,
88
+ displayName: displayName.trim(),
89
+ relationship: input.relationship as string | undefined,
90
+ importance,
91
+ responseExpectation: input.response_expectation as string | undefined,
92
+ preferredTone: input.preferred_tone as string | undefined,
93
+ channels,
94
+ });
95
+
96
+ const created = status === 201;
118
97
 
119
98
  return {
120
99
  content: `${created ? "Created" : "Updated"} contact:\n${formatContact(data.contact)}`,
121
100
  isError: false,
122
101
  };
123
102
  } catch (err) {
103
+ if (err instanceof GatewayRequestError) {
104
+ return { content: `Error: ${err.message}`, isError: true };
105
+ }
124
106
  const msg = err instanceof Error ? err.message : String(err);
125
107
  return { content: `Error: ${msg}`, isError: true };
126
108
  }
@@ -16,6 +16,10 @@
16
16
  "initial_content": {
17
17
  "type": "string",
18
18
  "description": "Initial Markdown content to populate the editor (optional)"
19
+ },
20
+ "reason": {
21
+ "type": "string",
22
+ "description": "Brief non-technical explanation of why this tool is being called"
19
23
  }
20
24
  }
21
25
  },
@@ -42,6 +46,10 @@
42
46
  "type": "string",
43
47
  "enum": ["replace", "append"],
44
48
  "description": "Whether to replace all content or append to the end. Defaults to append."
49
+ },
50
+ "reason": {
51
+ "type": "string",
52
+ "description": "Brief non-technical explanation of why this tool is being called"
45
53
  }
46
54
  },
47
55
  "required": ["surface_id", "content"]
@@ -2,7 +2,7 @@
2
2
  name: "Email Setup"
3
3
  description: "Create the assistant's own email address via the Vellum hosted API (one-time setup)"
4
4
  user-invocable: true
5
- metadata: {"vellum": {"emoji": "📧"}}
5
+ metadata: { "vellum": { "emoji": "📧" } }
6
6
  ---
7
7
 
8
8
  You are setting up your own personal email address. This is a one-time operation — once you have an email, you do not need to run this again.
@@ -16,17 +16,17 @@ Only proceed if the user explicitly asks you to create or set up **your own** (t
16
16
  Before doing anything, check whether you already have an email address configured:
17
17
 
18
18
  ```bash
19
- vellum email status --json
19
+ vellum email status
20
20
  ```
21
21
 
22
- Inspect `health.inboxes` in the response. If at least one inbox exists, tell the user the existing address and stop — do NOT create another one.
22
+ Inspect `addresses` in the response. If at least one address exists, tell the user the existing address and stop — do NOT create another one.
23
23
 
24
24
  ## Step 2: Create Your Email
25
25
 
26
- Create a new inbox through the domain CLI:
26
+ Create a new inbox through the CLI:
27
27
 
28
28
  ```bash
29
- vellum email inbox create --username <your-username> --json
29
+ vellum email inbox create --username <your-username>
30
30
  ```
31
31
 
32
32
  For `<your-username>`, use your assistant name (lowercased, alphanumeric only). Check your identity from `IDENTITY.md` or `USER.md` to determine your name. If you don't have a name yet, ask the user what username they'd like for your email.
@@ -36,10 +36,10 @@ Use the returned `inbox.address` (or `inbox.id` if `address` is empty) as the cr
36
36
  ## Step 3: Verify Status
37
37
 
38
38
  ```bash
39
- vellum email status --json
39
+ vellum email status
40
40
  ```
41
41
 
42
- Confirm the created inbox appears in `health.inboxes`.
42
+ Confirm the created inbox appears in `addresses`.
43
43
 
44
44
  ## Step 4: Confirm Setup
45
45
 
@@ -58,8 +58,11 @@ After the inbox is created and visible in status:
58
58
  ## Troubleshooting
59
59
 
60
60
  ### API key not configured
61
+
61
62
  If you get an error about a missing API key, the email provider has not been set up. Tell the user:
63
+
62
64
  > "Email isn't configured yet. Please set up the email integration first."
63
65
 
64
66
  ### Inbox creation failed
67
+
65
68
  If inbox creation returns an error (e.g. username taken), try a variation of the name (append a number or use a nickname) and retry once. If it still fails, report the error to the user.
@@ -32,6 +32,10 @@
32
32
  "reminder_cron_id": {
33
33
  "type": "string",
34
34
  "description": "Deprecated alias for reminder_schedule_id. Use reminder_schedule_id instead."
35
+ },
36
+ "reason": {
37
+ "type": "string",
38
+ "description": "Brief non-technical explanation of why this tool is being called"
35
39
  }
36
40
  },
37
41
  "required": ["channel", "thread_id"]
@@ -63,6 +67,10 @@
63
67
  "overdue_only": {
64
68
  "type": "boolean",
65
69
  "description": "When true, return only pending follow-ups past their expected response deadline"
70
+ },
71
+ "reason": {
72
+ "type": "string",
73
+ "description": "Brief non-technical explanation of why this tool is being called"
66
74
  }
67
75
  },
68
76
  "required": []
@@ -89,6 +97,10 @@
89
97
  "thread_id": {
90
98
  "type": "string",
91
99
  "description": "Thread ID to match (used with channel for auto-resolution)"
100
+ },
101
+ "reason": {
102
+ "type": "string",
103
+ "description": "Brief non-technical explanation of why this tool is being called"
92
104
  }
93
105
  },
94
106
  "required": []