UNPKG

eve

Version:

Filesystem-first framework for durable backend AI agents that run anywhere.

132 lines (95 loc) 7.64 kB
--- title: "Slack" description: "Reach your agent from Slack app mentions and DMs, with thread anchoring, buttons, and Vercel Connect credentials." type: integration --- The Slack channel puts your agent inside a workspace. It answers `@mentions` and DMs, replies in threads, shows typing indicators, and turns human-in-the-loop (HITL) prompts into buttons. Use it when the conversation should happen where your team already works. Credentials run through [Vercel Connect](../guides/auth-and-route-protection), which handles both the outbound bot token and inbound webhook verification, so there's no `SLACK_BOT_TOKEN` or `SLACK_SIGNING_SECRET` for you to manage. See [Channels](./overview) for the contract this builds on. ## Set up Connect Create a Slack Connect client and copy its UID (e.g. `slack/my-agent`), then attach this project as the trigger destination at eve's Slack route: ```bash npm install -g vercel@latest && export FF_CONNECT_ENABLED=1 vercel connect create slack --triggers vercel connect detach <uid> --yes vercel connect attach <uid> --triggers --trigger-path /eve/v1/slack --yes ``` `FF_CONNECT_ENABLED=1` turns on the Connect commands, which are feature-flagged in the Vercel CLI today. The `create` step provisions a destination at the default Connect path. `detach` then `attach --trigger-path /eve/v1/slack` re-points the trigger at the eve Slack route, since eve does not serve the default Connect path. `--triggers` turns on Slack Event Subscriptions; without it, Slack never delivers `app_mention` or `message.im`. You can also create the client from the [Connect dashboard](https://vercel.com/d?to=/%5Bteam%5D/~/connect&title=Go+to+Connect). ## Add the channel Scaffold the channel and its dependency with `eve channels add slack`, or set it up by hand: ```bash npm install @vercel/connect ``` ```ts title="agent/channels/slack.ts" import { connectSlackCredentials } from "@vercel/connect/eve"; import { slackChannel } from "eve/channels/slack"; export default slackChannel({ credentials: connectSlackCredentials("slack/my-agent"), }); ``` `connectSlackCredentials` returns `{ botToken, webhookVerifier }`, keeping token rotation, multi-workspace tenancy, and request verification inside Connect rather than your code. Deploy once the trigger destination and channel file are ready: ```bash VERCEL_USE_EXPERIMENTAL_FRAMEWORKS=1 vercel deploy --prod ``` `VERCEL_USE_EXPERIMENTAL_FRAMEWORKS=1` lets the Vercel CLI recognize eve as a framework during the build. eve's own setup commands set the same flag. ## How the channel handles messages ### Dispatch Inbound hooks decide whether to dispatch a turn and with what `auth`. Return `{ auth }` to dispatch, `null` to drop, or `{ auth, context }` to inject background into history. - `onAppMention(ctx, message)` handles `app_mention` events. The default derives workspace-scoped auth and posts a `Thinking…` indicator. - `onDirectMessage(ctx, message)` handles `message.im` events (needs `im:history` scope). Bot-authored messages and edits are filtered out first. - `onInteraction(action, ctx)` handles `block_actions` callbacks not consumed by HITL. You get the triggering mention by default, but not the earlier replies in the thread. Pull them in with `loadThreadContextMessages` and return them as `context`, which eve appends to history as user messages the model sees on every later turn. Use `since: "last-agent-reply"` so repeated mentions in one thread inject only what is new: ```ts import { defaultSlackAuth, loadThreadContextMessages, slackChannel } from "eve/channels/slack"; import { connectSlackCredentials } from "@vercel/connect/eve"; export default slackChannel({ credentials: connectSlackCredentials("slack/my-agent"), async onAppMention(ctx, message) { const auth = defaultSlackAuth(message, ctx); const prior = await loadThreadContextMessages(ctx.thread, message, { since: "last-agent-reply", }); if (prior.length === 0) return { auth }; const transcript = prior .map((m) => `${m.isMe ? "you" : (m.user ?? "user")}: ${m.markdown}`) .join("\n"); return { auth, context: [`Recent thread messages since your last reply:\n\n${transcript}`] }; }, }); ``` ### Delivery The default handlers reply in-thread and show progress. Typing indicators post automatically: `Thinking…` on inbound, `Working…` on `turn.started`, a truncated reasoning snippet on `reasoning.appended`, and tool status on `actions.requested`. Reasoning snippets build progressively: extensions of at least four characters appear immediately, while smaller streamed deltas use the five-second refresh interval to avoid one Slack request per token. Override `events["reasoning.appended"]` if you prefer generic wording. Override `onAppMention` or the `events` handlers to customize. When a session starts without a `threadTs` (say, from a schedule or `receive(slack, ...)`), it anchors on the first agent post, and later posts and mentions resume that same session. Pass `initialMessage` with a `Card` to land a structured anchor first instead. `threadTs` and `initialMessage` are mutually exclusive. The example below overrides `onAppMention` to gate on an authored message and posts the completed reply to the thread. Event handlers receive `(eventData, channel, ctx)`, with Slack platform handles on `channel.thread` and `channel.slack`: ```ts import { defaultSlackAuth, slackChannel } from "eve/channels/slack"; import { connectSlackCredentials } from "@vercel/connect/eve"; export default slackChannel({ credentials: connectSlackCredentials("slack/my-agent"), onAppMention: (ctx, message) => message.author ? { auth: defaultSlackAuth(message, ctx) } : null, events: { "message.completed"(eventData, channel, ctx) { if (eventData.finishReason === "tool-calls") return; if (eventData.message) channel.thread.post(eventData.message); }, }, }); ``` ### Human-in-the-loop (HITL) HITL renders as Slack buttons and selects. When the user responds, the parked session (paused awaiting input) resumes. Authorization prompts split public status from private credentials. A sign-in challenge (OAuth URL, device code) is a credential. Anyone who completes it binds their identity to the session's connection. The default `authorization.required` handler posts a public, link-free status in the thread, delivers the actual challenge ephemerally to the triggering user, device code included, and then updates that public status when `authorization.completed` fires. The handler receives a private-delivery context with `postEphemeral`, `postDirectMessage` (needs the `im:write` scope), and `state`. There is, intentionally, no public `post` and no raw API access. ```ts events: { "authorization.required"(eventData, channel) { const userId = channel.state.triggeringUserId; if (!userId || !eventData.authorization?.url) return; return channel.postDirectMessage(userId, `Sign in to continue: ${eventData.authorization.url}`); }, }, ``` ### Proactive sessions Start a session without an inbound message through `receive(slack, { message, target, auth })` from a schedule `run` handler, or `args.receive(slack, ...)` from another channel. The proactive target shape is `{ channelId }`. ### Attachments Inbound files behind authenticated Slack URLs are staged with `fetchFile`. See [File uploads](./custom#file-uploads) for the `fetchFile` contract. ## What to read next - [Channels overview](./overview): the channel contract and every built-in channel - [Auth & route protection](../guides/auth-and-route-protection): authenticating inbound traffic