experimental-ash 0.10.4 → 0.11.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,85 @@
1
1
  # experimental-ash
2
2
 
3
+ ## 0.11.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 6bc3fe0: fix(slack): allow `slackChannel` to handle whichever twin event arrives first
8
+
9
+ Slack delivers two webhook events for every `@bot` channel mention — one
10
+ `event.type: "message"` and one `event.type: "app_mention"`, both carrying
11
+ the same `event.ts`. The chat SDK dedupes incoming events on `message.id`
12
+ (= Slack `event.ts`), so only the first of the twin events reaches the
13
+ mention listener. The userland filter `if (raw?.type !== "app_mention")
14
+ return;` would then drop that surviving event whenever the `message`
15
+ twin arrived first, silently swallowing the mention. Removing the filter
16
+ lets the listener handle whichever twin arrives; both carry identical
17
+ text/channel/team. This also unblocks DMs, which only deliver `message`
18
+ events (no `app_mention` twin).
19
+
20
+ ## 0.11.0
21
+
22
+ ### Minor Changes
23
+
24
+ - c823956: refactor(slack): collapse `slack` and `slackChannel` into one `slackChannel()`; merge `run` + `onMention` into a single mention hook
25
+
26
+ The previous `slack` (defaults wrapper) and `slackChannel` (raw config) factories are unified behind a single `slackChannel(config?)` exported from `experimental-ash/channels/slack`. Defaults always apply; any field you supply replaces the corresponding default. Events you don't override keep their defaults. The default `onMention` derives a workspace-scoped auth from the inbound Slack actor and posts a `"Thinking…"` typing indicator before the workflow runtime cold-starts.
27
+
28
+ The separate `run` (gate + auth) and `onMention` (pre-dispatch side effects) hooks are merged into a single `onMention(ctx, message)` callback. Return `{ auth }` to dispatch, `null` to drop, and perform any side effects inline. Errors thrown from `onMention` are caught, logged, and drop the mention — previously side-effect throws were logged but dispatch continued. Wrap best-effort side effects in `try/catch` if you want them to be non-fatal.
29
+
30
+ Migration:
31
+
32
+ ```ts
33
+ // before — slackChannel with separate run + onMention
34
+ import { slackChannel } from "experimental-ash/channels/slack";
35
+
36
+ export default slackChannel({
37
+ run(ctx, message) {
38
+ if (!ALLOWED.has(ctx.slack.channelId)) return null;
39
+ return {
40
+ auth: {
41
+ /* ... */
42
+ },
43
+ };
44
+ },
45
+ onMention(ctx) {
46
+ ctx.thread.startTyping("Thinking…");
47
+ },
48
+ events: {
49
+ /* ... */
50
+ },
51
+ });
52
+
53
+ // before — slack() with defaults
54
+ import { slack } from "experimental-ash/channels/slack";
55
+
56
+ export default slack({
57
+ events: {
58
+ /* ... */
59
+ },
60
+ });
61
+
62
+ // after — one factory, one onMention hook
63
+ import { slackChannel } from "experimental-ash/channels/slack";
64
+
65
+ export default slackChannel({
66
+ onMention(ctx, message) {
67
+ if (!ALLOWED.has(ctx.slack.channelId)) return null;
68
+ ctx.thread.startTyping("Thinking…");
69
+ return {
70
+ auth: {
71
+ /* ... */
72
+ },
73
+ };
74
+ },
75
+ events: {
76
+ /* ... */
77
+ },
78
+ });
79
+ ```
80
+
81
+ Drops the `SlackOptions` type and the `run` / `onMention` split on `SlackChannelConfig`. Adds `SlackMentionResult` (`{ auth } | null`) and `SlackMentionResultOrPromise`.
82
+
3
83
  ## 0.10.4
4
84
 
5
85
  ### Patch Changes
package/README.md CHANGED
@@ -49,7 +49,7 @@ Every authored directory has a typed helper. Import each from the matching subpa
49
49
  | `defineSkill(...)`, `getSkill(...)` | `experimental-ash/skills` | `skills/<name>.ts` (or `skills/<name>.md`) |
50
50
  | `defineHook(...)` | `experimental-ash/hooks` | `hooks/<slug>.ts` |
51
51
  | `defineChannel(...)`, `POST`, `GET` | `experimental-ash/channels` | `channels/<name>.ts` |
52
- | `ashChannel(...)`, `slack`, `slackChannel(...)`, `vercelOidc(...)` | `experimental-ash/channels/ash`, `/slack`, `/auth` | reused from `channels/<name>.ts` |
52
+ | `ashChannel(...)`, `slackChannel(...)`, `vercelOidc(...)` | `experimental-ash/channels/ash`, `/slack`, `/auth` | reused from `channels/<name>.ts` |
53
53
  | `defineSandbox(...)` | `experimental-ash/sandbox` | `sandbox.ts` (or `sandbox/sandbox.ts`) |
54
54
  | `defineSchedule(...)` | `experimental-ash/schedules` | `schedules/<name>.ts` (or `schedules/<name>.md`) |
55
55
  | `defineEvalSuite(...)` | `experimental-ash/evals` | `evals/<name>.eval.ts` |
@@ -109,29 +109,33 @@ export default ashChannel({
109
109
 
110
110
  ## Slack Channels
111
111
 
112
- For Slack, Ash provides three levels of abstraction:
112
+ Slack channels are authored with a single `slackChannel()` factory. Pass no config for the
113
+ zero-config defaults, or supply only the fields you want to override -- everything else keeps the
114
+ default.
113
115
 
114
- ### `slack()` -- zero-config
116
+ ### Zero-config
115
117
 
116
118
  ```ts
117
- import { slack } from "experimental-ash/channels/slack";
119
+ import { slackChannel } from "experimental-ash/channels/slack";
118
120
 
119
- export default slack();
121
+ export default slackChannel();
120
122
  ```
121
123
 
122
- Handles mentions, typing indicators, message delivery, and HITL interactions out of the box.
124
+ Handles mentions, typing indicators, message delivery, and HITL interactions out of the box. The
125
+ default `onMention` derives auth from the inbound Slack actor and posts a `"Thinking…"` typing
126
+ indicator before the workflow runtime starts.
123
127
 
124
- ### `slackChannel({ run, events })` -- custom config
128
+ ### Custom config
125
129
 
126
- When you need custom rendering or event handling, use `slackChannel` with a config object. The `run`
127
- function receives the channel context and the inbound message and returns run options like `auth`.
128
- Event handlers are declared under the `events` key.
130
+ When you need custom rendering or event handling, pass a config object. `onMention` decides whether
131
+ to dispatch and with what auth; `events` lets you replace individual event handlers; `onInteraction`
132
+ handles non-HITL button clicks.
129
133
 
130
134
  ```ts
131
135
  import { slackChannel } from "experimental-ash/channels/slack";
132
136
 
133
137
  export default slackChannel({
134
- run(ctx, message) {
138
+ onMention(ctx, message) {
135
139
  return {
136
140
  auth: {
137
141
  principalId: message.author.userId,
@@ -157,12 +161,16 @@ export default slackChannel({
157
161
  });
158
162
  ```
159
163
 
164
+ Fields you supply fully replace the corresponding default -- if you pass your own
165
+ `"message.completed"` handler, the default's behavior for that event is gone. Other defaults stay
166
+ in place.
167
+
160
168
  ### Interactions
161
169
 
162
170
  Interactions (button clicks, modals) are platform events, not agent events. They do not appear in the
163
171
  event handlers.
164
172
 
165
- `slackChannel` handles interactions separately:
173
+ `slackChannel()` handles interactions in two paths:
166
174
 
167
175
  - **HITL interactions** (approval buttons matching pending input requests) are delivered to the agent
168
176
  automatically. The agent resumes.
@@ -172,7 +180,7 @@ event handlers.
172
180
  import { slackChannel } from "experimental-ash/channels/slack";
173
181
 
174
182
  export default slackChannel({
175
- run(ctx, message) {
183
+ onMention(ctx, message) {
176
184
  return {
177
185
  auth: {
178
186
  principalId: message.author.userId,
@@ -194,21 +202,17 @@ export default slackChannel({
194
202
  });
195
203
  ```
196
204
 
197
- ### The three shapes
205
+ ### The two shapes
198
206
 
199
207
  ```
200
- defineChannel({ routes: [...], events: {...} }) -- raw: you handle HTTP
201
- slackChannel({ run, events: {...}, onInteraction })-- slack: you handle events
202
- slack() -- default: everything handled
208
+ defineChannel({ routes: [...], events: {...} }) -- raw: you handle HTTP
209
+ slackChannel({ onMention?, events?, onInteraction? }) -- slack: everything wired, override as needed
203
210
  ```
204
211
 
205
- Each compiles down to the one below it:
206
-
207
- - `slack()` -> `slackChannel(defaultConfig)`
208
- - `slackChannel(config)` -> `defineChannel` + Chat SDK setup + typed event dispatch
209
- - `defineChannel` -> the primitive
212
+ `slackChannel(config)` compiles down to `defineChannel` plus the Chat SDK setup and typed event
213
+ dispatch.
210
214
 
211
- For a Slack app backed by Vercel Connex, see [Slack channel setup](./slack.md) to create the Connex client
215
+ For a Slack app backed by Vercel Connect, see [Slack channel setup](./slack.md) to create the Connect client
212
216
  and channel file.
213
217
 
214
218
  ## File Uploads
@@ -1,38 +1,38 @@
1
1
  ---
2
2
  title: "Slack channel setup"
3
- description: "Create a Slack-backed Ash channel with Vercel Connex."
3
+ description: "Create a Slack-backed Ash channel with Vercel Connect."
4
4
  type: integration
5
5
  related:
6
6
  - /channels
7
7
  - /connections
8
8
  ---
9
9
 
10
- Ash Slack channels can use Vercel Connex for both outbound Slack bot tokens and inbound webhook
11
- verification. With Connex, you do not need to manage `SLACK_BOT_TOKEN` or `SLACK_SIGNING_SECRET`
10
+ Ash Slack channels can use Vercel Connect for both outbound Slack bot tokens and inbound webhook
11
+ verification. With Connect, you do not need to manage `SLACK_BOT_TOKEN` or `SLACK_SIGNING_SECRET`
12
12
  environment variables yourself.
13
13
 
14
14
  ## Prerequisites
15
15
 
16
16
  - A Vercel project for the agent.
17
17
  - A Slack workspace where you can install the app.
18
- - Access to Vercel Connex for your team.
18
+ - Access to Vercel Connect for your team.
19
19
 
20
- For local development, link the project and pull Vercel OIDC env vars so Connex can authenticate to
21
- the Vercel API:
20
+ For local development, link the project and pull Vercel OIDC env vars so Connect can authenticate
21
+ to the Vercel API:
22
22
 
23
23
  ```bash
24
24
  vercel link
25
25
  vercel env pull
26
26
  ```
27
27
 
28
- ## Create The Connex Client
28
+ ## Create The Connect Client
29
29
 
30
- Create a Slack Connex client, then copy its UID. The UID is the value you pass to Ash, for example
30
+ Create a Slack Connect client, then copy its UID. The UID is the value you pass to Ash, for example
31
31
  `slack/my-agent`.
32
32
 
33
33
  ### Dashboard Path
34
34
 
35
- Open [Connex in the Vercel dashboard](https://vercel.com/d?to=/%5Bteam%5D/~/connex&title=Go+to+Connex),
35
+ Open [Connect in the Vercel dashboard](https://vercel.com/d?to=/%5Bteam%5D/~/connect&title=Go+to+Connect),
36
36
  then:
37
37
 
38
38
  1. Choose **Create Client**.
@@ -44,20 +44,20 @@ then:
44
44
 
45
45
  ### CLI Or Agent-Assisted Path
46
46
 
47
- The `vercel connex` command is currently gated for allow-listed teams. If the command is missing,
48
- update the Vercel CLI and enable the Connex flag in the terminal session where you will run the
47
+ The `vercel connect` command is currently gated for allow-listed teams. If the command is missing,
48
+ update the Vercel CLI and enable the Connect flag in the terminal session where you will run the
49
49
  commands:
50
50
 
51
51
  ```bash
52
52
  pnpm i -g vercel@latest
53
- export FF_CONNEX_ENABLED=1
54
- vercel connex --help
53
+ export FF_CONNECT_ENABLED=1
54
+ vercel connect --help
55
55
  ```
56
56
 
57
57
  The flag only applies to the current shell session. Export it again in new terminals, or add it to
58
- your shell config while you are working with Connex.
58
+ your shell config while you are working with Connect.
59
59
 
60
- Make sure the CLI is authenticated and scoped to the Vercel team that has Connex access:
60
+ Make sure the CLI is authenticated and scoped to the Vercel team that has Connect access:
61
61
 
62
62
  ```bash
63
63
  vercel login
@@ -65,34 +65,34 @@ vercel switch
65
65
  vercel link
66
66
  ```
67
67
 
68
- If you are using an AI coding agent, install the Connex skill first:
68
+ If you are using an AI coding agent, install the Connect skill first:
69
69
 
70
70
  ```bash
71
- npx skills add https://github.com/vercel/connex --skill vercel-connex
71
+ npx skills add https://github.com/vercel/connect --skill vercel-connect
72
72
  ```
73
73
 
74
74
  Then create the Slack client from the project or agent folder that will use it:
75
75
 
76
76
  ```bash
77
- vercel connex create slack
77
+ vercel connect create slack
78
78
  ```
79
79
 
80
- Run Connex commands from the directory containing the agent's `package.json` or `vercel.json`.
81
- Connex uses that project context to configure project access, webhooks, and triggers. The command may
82
- open a browser for Slack installation or OAuth consent; finish that flow before continuing.
80
+ Run Connect commands from the directory containing the agent's `package.json` or `vercel.json`.
81
+ Connect uses that project context to configure project access, webhooks, and triggers. The command
82
+ may open a browser for Slack installation or OAuth consent; finish that flow before continuing.
83
83
 
84
84
  You can list existing clients with:
85
85
 
86
86
  ```bash
87
- vercel connex list
87
+ vercel connect list
88
88
  ```
89
89
 
90
- ## Install Connex
90
+ ## Install Connect
91
91
 
92
- Add the Connex SDK to the Ash project:
92
+ Add the Connect SDK to the Ash project:
93
93
 
94
94
  ```bash
95
- pnpm add @vercel/connex
95
+ pnpm add @vercel/connect
96
96
  ```
97
97
 
98
98
  ## Add The Slack Channel File
@@ -100,35 +100,36 @@ pnpm add @vercel/connex
100
100
  Create `agent/channels/slack.ts`:
101
101
 
102
102
  ```ts
103
- import { slackChannelCredentials } from "@vercel/connex/ash";
104
- import { slack } from "experimental-ash/channels/slack";
103
+ import { connectSlackCredentials } from "@vercel/connect/ash";
104
+ import { slackChannel } from "experimental-ash/channels/slack";
105
105
 
106
- export default slack({
107
- credentials: slackChannelCredentials("slack/my-agent"),
106
+ export default slackChannel({
107
+ credentials: connectSlackCredentials("slack/my-agent"),
108
108
  });
109
109
  ```
110
110
 
111
- Replace `slack/my-agent` with the UID from the Connex client.
111
+ Replace `slack/my-agent` with the UID from the Connect client.
112
112
 
113
113
  The helper returns a complete Slack credentials object:
114
114
 
115
- - `botToken` resolves an app-scoped Slack token through Connex for outbound posts.
116
- - `webhookVerifier` verifies Connex-forwarded Slack webhooks with Vercel OIDC.
115
+ - `botToken` resolves an app-scoped Slack token through Connect for outbound posts.
116
+ - `webhookVerifier` verifies Connect-forwarded Slack webhooks with Vercel OIDC.
117
117
 
118
- That means token rotation, refresh, multi-workspace tenancy, and inbound request verification stay in
119
- Connex instead of in your app environment.
118
+ That means token rotation, refresh, multi-workspace tenancy, and inbound request verification stay
119
+ in Connect instead of in your app environment.
120
120
 
121
121
  ## Customize Delivery
122
122
 
123
- If you need custom Slack rendering or interaction handling, use `slackChannel` with a config object:
123
+ If you need custom Slack rendering or interaction handling, pass a config object to `slackChannel()`.
124
+ Any field you supply replaces the corresponding default; fields you omit keep the defaults.
124
125
 
125
126
  ```ts
126
- import { slackChannelCredentials } from "@vercel/connex/ash";
127
+ import { connectSlackCredentials } from "@vercel/connect/ash";
127
128
  import { slackChannel } from "experimental-ash/channels/slack";
128
129
 
129
130
  export default slackChannel({
130
- credentials: slackChannelCredentials("slack/my-agent"),
131
- run(ctx, message) {
131
+ credentials: connectSlackCredentials("slack/my-agent"),
132
+ onMention(ctx, message) {
132
133
  return {
133
134
  auth: {
134
135
  principalId: message.author.userId,
@@ -152,15 +153,15 @@ export default slackChannel({
152
153
 
153
154
  ## Hooks
154
155
 
155
- `slackChannel` exposes three places to plug in custom behavior:
156
+ `slackChannel()` exposes two places to plug in custom behavior on the inbound webhook side:
156
157
 
157
- - **`run(ctx, message)`** -- decides whether to dispatch a turn for an inbound `app_mention` and
158
- with what `auth` context. Returns `{ auth }` to dispatch or `null` to silently drop the mention.
159
- May be sync or async; the framework awaits the result before dispatching.
160
- - **`onMention(ctx, message)`** -- free-form pre-dispatch side effects, invoked on the inbound
161
- webhook side after `run()` accepts and before the turn is enqueued. Use it for work that should
162
- fire immediately, before the workflow runtime cold-starts -- starting a typing indicator,
163
- logging, etc. Errors are caught and logged; the turn still dispatches.
158
+ - **`onMention(ctx, message)`** -- decides whether to dispatch a turn for an inbound `app_mention`,
159
+ with what `auth` context, and runs any pre-dispatch side effects (typing indicators, logging,
160
+ feature-flag lookups). Return `{ auth }` to dispatch or `null` to silently drop the mention. May
161
+ be sync or async; the framework awaits the result before dispatching. Thrown errors are caught,
162
+ logged, and drop the mention -- wrap best-effort side effects in `try/catch` if you want them to
163
+ be non-fatal. The default `onMention` derives a workspace-scoped auth from the Slack actor and
164
+ posts a `"Thinking…"` typing indicator.
164
165
  - **`onInteraction(action, ctx)`** -- handler for Slack `block_actions` callbacks (button clicks,
165
166
  selects, etc.) that are not consumed by the framework's HITL pipeline. Runs on the inbound
166
167
  webhook side via `waitUntil`, so the channel returns `200 OK` to Slack immediately.
@@ -180,11 +181,11 @@ connection pool:
180
181
 
181
182
  ```ts
182
183
  import { createRedisState } from "@chat-adapter/state-redis";
183
- import { slack } from "experimental-ash/channels/slack";
184
+ import { slackChannel } from "experimental-ash/channels/slack";
184
185
 
185
186
  const stateAdapter = createRedisState({ url: process.env.REDIS_URL! });
186
187
 
187
- export default slack({
188
+ export default slackChannel({
188
189
  stateAdapter,
189
190
  });
190
191
  ```
@@ -194,8 +195,8 @@ so state stays coherent within a single agent run.
194
195
 
195
196
  ## Typing Indicators
196
197
 
197
- Out of the box, `slack()` posts typing statuses so the user sees feedback before the agent starts
198
- thinking:
198
+ Out of the box, `slackChannel()` posts typing statuses so the user sees feedback before the agent
199
+ starts thinking:
199
200
 
200
201
  - **`Thinking...`** -- posted by the default `onMention` hook the moment a mention arrives, on the
201
202
  inbound webhook side, before the workflow runtime starts.
@@ -204,14 +205,17 @@ thinking:
204
205
  - **Tool status** -- when an `actions.requested` event fires, the default handler updates the typing
205
206
  indicator to show which tools are running.
206
207
 
207
- To customize typing indicators, use `slackChannel` and supply your own `onMention` plus event
208
- handlers:
208
+ To customize typing indicators, override `onMention` and/or specific event handlers. Replacing one
209
+ handler does not affect the others -- the rest keep their defaults.
209
210
 
210
211
  ```ts
211
212
  import { slackChannel } from "experimental-ash/channels/slack";
212
213
 
213
214
  export default slackChannel({
214
- run(_ctx, message) {
215
+ async onMention(ctx, message) {
216
+ await ctx.thread.startTyping(
217
+ message.text.includes("weather") ? "Checking the weather..." : "Thinking...",
218
+ );
215
219
  return {
216
220
  auth: {
217
221
  principalId: message.author.userId,
@@ -221,23 +225,11 @@ export default slackChannel({
221
225
  },
222
226
  };
223
227
  },
224
- async onMention(ctx, message) {
225
- await ctx.thread.startTyping(
226
- message.text.includes("weather") ? "Checking the weather..." : "Thinking...",
227
- );
228
- },
229
228
  events: {
230
229
  async "actions.requested"(event, ctx) {
231
230
  const labels = event.actions.map((a) => (a.kind === "tool-call" ? a.toolName : a.kind));
232
231
  await ctx.thread.startTyping(`Running ${labels.join(", ")}...`);
233
232
  },
234
- async "message.completed"(event, ctx) {
235
- if (event.finishReason === "tool-calls") return;
236
- if (event.message) await ctx.thread.post(event.message);
237
- },
238
- async "session.failed"(_event, ctx) {
239
- await ctx.thread.post("Something went wrong.");
240
- },
241
233
  },
242
234
  });
243
235
  ```
@@ -15,11 +15,11 @@ The model sees each connection through `connection_search` and picks tools based
15
15
  Every connection needs to decide how Ash obtains a bearer token for the MCP server. Pick one before
16
16
  you write the file — the rest of the shape is the same:
17
17
 
18
- | Strategy | Use it when |
19
- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
20
- | [Static token](#static-tokens) | You already have an API key or pre-provisioned JWT (env var, secrets manager, vault). |
21
- | [Connex-managed OAuth](#oauth-via-connex) | The server uses OAuth and you want Vercel to own consent, token storage, and refresh. |
22
- | [No auth](#no-auth) | The server doesn't require a token — localhost, public servers, or servers already authenticated through [`headers`](#headers). |
18
+ | Strategy | Use it when |
19
+ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
20
+ | [Static token](#static-tokens) | You already have an API key or pre-provisioned JWT (env var, secrets manager, vault). |
21
+ | [Connect-managed OAuth](#oauth-via-connect) | The server uses OAuth and you want Vercel to own consent, token storage, and refresh. |
22
+ | [No auth](#no-auth) | The server doesn't require a token — localhost, public servers, or servers already authenticated through [`headers`](#headers). |
23
23
 
24
24
  If you need something the table doesn't cover (BYO token vault, custom signing flow, on-demand JWT
25
25
  exchange), use the static-token shape with whatever async logic you need inside `getToken`.
@@ -51,27 +51,27 @@ Ash defaults static `getToken`-only auth to `principalType: "app"`, which keys t
51
51
  cache by `"app"` so all sessions share the same credential. Set `principalType: "user"` when each
52
52
  end-user has their own token (see [Principal Type](#principal-type-user-vs-app)).
53
53
 
54
- ## OAuth via Connex
54
+ ## OAuth via Connect
55
55
 
56
- For OAuth-backed connections, [Vercel Connex](https://vercel.com/docs/connex) can own the OAuth
57
- flow, encrypt token storage, and handle refresh. The `connex` helper from `@vercel/connex/ash`
58
- collapses that into a single declaration and wires Connex's typed errors into Ash's
56
+ For OAuth-backed connections, [Vercel Connect](https://vercel.com/docs/connect) can own the OAuth
57
+ flow, encrypt token storage, and handle refresh. The `connect` helper from `@vercel/connect/ash`
58
+ collapses that into a single declaration and wires Connect's typed errors into Ash's
59
59
  `ConnectionAuthorizationRequiredError` / `ConnectionAuthorizationFailedError` so the runtime can
60
60
  drive consent without any connection-specific glue.
61
61
 
62
- **Prerequisites.** Connex is currently in private beta — your team needs to be enabled for it before
63
- the helper will work. Once you have access:
62
+ **Prerequisites.** Connect is currently in private beta — your team needs to be enabled for it
63
+ before the helper will work. Once you have access:
64
64
 
65
65
  1. Install the SDK:
66
66
 
67
67
  ```bash
68
- pnpm add @vercel/connex
68
+ pnpm add @vercel/connect
69
69
  ```
70
70
 
71
- 2. Create the Connex client — either in the Vercel dashboard or with
72
- `vercel connex create <type> --name <uid>` from the CLI. Pick a `uid` (e.g. `"linear"`); this is
73
- the value you'll pass to `connex()`.
74
- 3. Link the Connex client to the Vercel project that runs your agent.
71
+ 2. Create the Connect client — either in the Vercel dashboard or with
72
+ `vercel connect create <type> --name <uid>` from the CLI. Pick a `uid` (e.g. `"linear"`); this is
73
+ the value you'll pass to `connect()`.
74
+ 3. Link the Connect client to the Vercel project that runs your agent.
75
75
  4. Run `vercel link` and `vercel env pull` so `VERCEL_OIDC_TOKEN` is available locally — the SDK
76
76
  uses it to authenticate to the Vercel API. On Vercel deployments OIDC is auto-provisioned.
77
77
 
@@ -79,24 +79,25 @@ Then declare the connection:
79
79
 
80
80
  ```ts
81
81
  // agent/connections/linear.ts
82
- import { connex } from "@vercel/connex/ash";
82
+ import { connect } from "@vercel/connect/ash";
83
83
  import { defineMcpClientConnection } from "experimental-ash/connections";
84
84
 
85
85
  export default defineMcpClientConnection({
86
86
  url: "https://mcp.linear.app/sse",
87
87
  description: "Linear workspace — issues, projects, cycles, and comments.",
88
- auth: connex("linear"),
88
+ auth: connect("linear"),
89
89
  });
90
90
  ```
91
91
 
92
- `"linear"` is the UID you chose when registering the Connex client (any string that doesn't start
93
- with a Vercel-reserved prefix). Use the object form when you need to pass additional Connex options:
92
+ `"linear"` is the UID you chose when registering the Connect client (any string that doesn't start
93
+ with a Vercel-reserved prefix). Use the object form when you need to pass additional Connect
94
+ options:
94
95
 
95
96
  ```ts
96
- auth: connex({ clientId: "linear" }),
97
+ auth: connect({ clientId: "linear" }),
97
98
  ```
98
99
 
99
- Connex-managed OAuth defaults to user-scoped auth — each end-user authorizes through their own
100
+ Connect-managed OAuth defaults to user-scoped auth — each end-user authorizes through their own
100
101
  browser, and the runtime resolves the per-user token before each tool call.
101
102
 
102
103
  ## No Auth
@@ -113,7 +114,7 @@ export default defineMcpClientConnection({
113
114
 
114
115
  ## Principal Type: `user` vs `app`
115
116
 
116
- Connex-managed OAuth defaults to user-scoped auth. Custom `getToken` auth may set `principalType`
117
+ Connect-managed OAuth defaults to user-scoped auth. Custom `getToken` auth may set `principalType`
117
118
  when it needs a different cache and principal model:
118
119
 
119
120
  - `principalType: "user"` returns an interactive definition. Each end-user authorizes the
@@ -151,7 +152,7 @@ import { once } from "experimental-ash/tools/approval";
151
152
  export default defineMcpClientConnection({
152
153
  url: "https://mcp.linear.app/sse",
153
154
  description: "Linear workspace.",
154
- auth: connex("linear"),
155
+ auth: connect("linear"),
155
156
  approval: once(),
156
157
  });
157
158
  ```
@@ -168,7 +169,7 @@ Constrain which tools the model sees with `tools.allow` or `tools.block`:
168
169
  export default defineMcpClientConnection({
169
170
  url: "https://mcp.linear.app/sse",
170
171
  description: "Linear — read-only.",
171
- auth: connex("linear"),
172
+ auth: connect("linear"),
172
173
  tools: { allow: ["search_issues", "get_issue"] },
173
174
  });
174
175
  ```
@@ -156,7 +156,7 @@ Use:
156
156
  - `lib/` for shared helper modules imported by the slots above
157
157
 
158
158
  See [Connections](./connections.md) for the connection authorization shape, including the
159
- `@vercel/connex/ash` helper for OAuth-backed connections.
159
+ `@vercel/connect/ash` helper for OAuth-backed connections.
160
160
 
161
161
  ## What To Read Next
162
162
 
@@ -110,9 +110,9 @@ See [Skills](./skills.md) for the full authoring model.
110
110
  ## Passing Custom Context From a Channel
111
111
 
112
112
  Channels can inject custom typed durable context into the agent via the channel's `state` and
113
- `context()` properties on `defineChannel`, or via `run()` options on `slackChannel`. This is useful
114
- when a channel needs to pass platform-specific metadata (tenant ID, feature flags, etc.) that
115
- authored tools can read on the same turn and on later turns.
113
+ `context()` properties on `defineChannel`, or via the auth attributes returned from `onMention` on
114
+ `slackChannel()`. This is useful when a channel needs to pass platform-specific metadata (tenant
115
+ ID, feature flags, etc.) that authored tools can read on the same turn and on later turns.
116
116
 
117
117
  ### Defining a key
118
118
 
@@ -129,7 +129,7 @@ export const TenantKey = new ContextKey<string>("myapp.tenant");
129
129
 
130
130
  ### Setting context from a channel
131
131
 
132
- Use `slackChannel` and seed context via the `run()` return value or the channel `state`:
132
+ Use `slackChannel()` and seed context via the `onMention` return value's auth attributes:
133
133
 
134
134
  `agent/channels/slack.ts`
135
135
 
@@ -138,7 +138,7 @@ import { slackChannel } from "experimental-ash/channels/slack";
138
138
  import { TenantKey } from "./keys.js";
139
139
 
140
140
  export default slackChannel({
141
- run(ctx, message) {
141
+ onMention(ctx, message) {
142
142
  const tenantId = lookupTenant(message);
143
143
  return {
144
144
  auth: {
@@ -149,17 +149,19 @@ export default slackChannel({
149
149
  },
150
150
  };
151
151
  },
152
- "message.completed"(event, ctx) {
153
- if (event.finishReason === "tool-calls") return;
154
- if (event.message) ctx.thread.post(event.message);
155
- },
156
- "session.failed"(event, ctx) {
157
- ctx.thread.post("Something went wrong.");
152
+ events: {
153
+ "message.completed"(event, ctx) {
154
+ if (event.finishReason === "tool-calls") return;
155
+ if (event.message) ctx.thread.post(event.message);
156
+ },
157
+ "session.failed"(event, ctx) {
158
+ ctx.thread.post("Something went wrong.");
159
+ },
158
160
  },
159
161
  });
160
162
  ```
161
163
 
162
- Auth lives on the return value of `run()`, not on a separate channel object.
164
+ Auth lives on the return value of `onMention`, not on a separate channel object.
163
165
 
164
166
  ### Reading context from a tool
165
167