Files
2026-07-13 12:58:18 +08:00

354 lines
16 KiB
Markdown

# @copilotkit/channels-slack
The **Slack `PlatformAdapter`** for [`@copilotkit/channels`](../channels). It connects a
Slack workspace to any AG-UI agent: ingress via Bolt (Socket Mode), egress as
Block Kit rendered from the `@copilotkit/channels-ui` JSX vocabulary, plus text
streaming, opaque-id interactions, and HITL.
You write your UI as JSX once (`@copilotkit/channels-ui`) and drive the bot with
`@copilotkit/channels`; this package is the only one that talks to Slack.
## Install
```sh
pnpm add @copilotkit/channels-slack @copilotkit/channels @copilotkit/channels-ui
```
## Quickstart
```ts
import { createBot } from "@copilotkit/channels";
import {
slack,
defaultSlackTools,
defaultSlackContext,
} from "@copilotkit/channels-slack";
const bot = createBot({
adapters: [
slack({
botToken: process.env.SLACK_BOT_TOKEN!, // xoxb-…
appToken: process.env.SLACK_APP_TOKEN!, // xapp-… (Socket Mode)
}),
],
agent: (threadId) => makeAgent(threadId),
tools: [...defaultSlackTools, ...appTools], // lookup_slack_user + your tools
context: [...defaultSlackContext, ...appContext], // tagging/mrkdwn/thread guidance
});
bot.onMention(({ thread }) => thread.runAgent());
await bot.start();
```
`slack(opts)` returns a `SlackAdapter`. By default it runs in **Socket Mode**
(`socketMode: true`) — outbound WebSocket only, no public URL needed. HTTP
mode (`socketMode: false`) needs `signingSecret` and a `port`. The Slack
listener pre-filters ingress to the turns the bot should answer. By default,
DMs are conversational, app mentions respond in-thread, and plain replies in
channel/private-channel threads require another app mention.
### Required env
| Var | Token | Purpose |
| ----------------- | ------- | -------------------------------- |
| `SLACK_BOT_TOKEN` | `xoxb-` | Bot token for the Web API. |
| `SLACK_APP_TOKEN` | `xapp-` | App-level token for Socket Mode. |
## Response routing
Use `respondTo` to choose which Slack message events become `onMention` turns:
| Surface | Default behavior | Option |
| --------------------------------------- | ----------------------- | ----------------------------------------------------- |
| Direct messages (`message.im`) | Respond | `respondTo.directMessages` |
| App mentions (`app_mention`) | Respond in-thread | `respondTo.appMentions` / `appMentions.reply` |
| Plain channel/private-channel replies | Ignore unless mentioned | `respondTo.threadReplies: "afterBotReply"` for legacy |
| Assistant pane | Separate default-on API | `assistant`; not controlled by `respondTo` |
| Slash commands, reactions, interactions | Explicit trigger paths | Not controlled by `respondTo` |
```ts
// Default routing made explicit.
slack({
botToken,
appToken,
respondTo: {
directMessages: true,
appMentions: { reply: "thread" },
threadReplies: "mentionsOnly",
},
});
```
```ts
// Legacy owned-thread continuation.
slack({
botToken,
appToken,
respondTo: {
threadReplies: "afterBotReply",
},
});
```
For the default mention-only thread behavior, subscribe to `app_mention` and
`message.im` events. Add `message.channels` and `message.groups` only when you
enable `respondTo.threadReplies: "afterBotReply"` and want Slack to deliver
plain channel/private-channel thread replies.
## What it provides
### JSX → Block Kit rendering
`renderSlackMessage(ir)` / `renderBlockKit(ir)` translate the
`@copilotkit/channels-ui` vocabulary to Block Kit: `Message → blocks`,
`Header → header`, `Section → section (mrkdwn)`, `Markdown → markdownToMrkdwn`,
`Field(s) → section.fields`, `Context → context`, `Actions → actions`,
`Button → button (action_id = minted opaque id)`, `Select → static_select`,
`Input → plain_text_input`, `Image → image`, `Divider → divider`.
### Per-element budget
Slack caps every element. The renderer degrades by truncate-with-overflow /
clamp — it never silently drops content. Limits live in `SLACK_LIMITS`:
| Limit | Value | Element |
| ------------------ | ----- | -------------------------- |
| `blocksPerMessage` | 50 | blocks per message |
| `sectionText` | 3000 | section body chars |
| `headerText` | 150 | header chars |
| `fieldsPerSection` | 10 | fields per section |
| `fieldText` | 2000 | field chars |
| `actionsElements` | 25 | controls per actions row |
| `contextElements` | 10 | elements per context block |
| `buttonText` | 75 | button label chars |
| `actionId` | 255 | `action_id` chars |
| `buttonValue` | 2000 | button value chars |
| `selectOptions` | 100 | options per select |
### Colored cards
`<Message accent="#RRGGBB">` renders as a Slack attachment with a colored
left bar (Block Kit blocks have no native accent, so accented messages are
posted as `attachments: [{ color, blocks }]`).
### Streaming
By default, replies stream via Slack's **native streaming API**
(`chat.startStream` / `appendStream` / `stopStream`) wherever the reply target
is a thread — a true streaming UI rendering **raw markdown** (so real tables and
fenced code render natively). A whole turn streams into **one** message: text
from every step accumulates into a single bubble (Slack documents only a 12k
char limit _per append_, with no cumulative cap, so there is no multi-message
splitting), and tool calls surface as native in-message **`task_update`**
chunks (a "timeline" of `Using …``Used …` steps) instead of separate status
messages. Workspaces where structured chunks aren't available degrade
automatically to `:wrench:` status rows.
Flat DMs (no thread) and any workspace where the streaming API is unavailable
fall back automatically to the shipped `chat.update` transport (throttled edits,
multi-message chunking, mid-stream bracket auto-close, Markdown → mrkdwn
translation). Pass `streaming: "legacy"` to force the `chat.update` transport
everywhere. The fallback is transparent — **opting in can never break a bot**:
the first `startStream` failure marks the workspace legacy and redoes the stream
the old way.
### Feedback buttons (opt-in)
Pass `feedback` to attach Slack's native AI feedback row (👍/👎,
`context_actions` + `feedback_buttons`) to each finalized streamed reply. Clicks
are routed straight to your handler — they never reach the engine's interaction
dispatch. Without `feedback`, no buttons are shown.
```ts
slack({
botToken,
appToken,
feedback: {
onFeedback: ({ sentiment, user, channel, messageTs }) => {
recordFeedback({ sentiment, user, channel, messageTs }); // your telemetry
},
// positiveLabel / negativeLabel are optional
},
});
```
The row is attached at `chat.stopStream` (the only streaming call that accepts
`blocks`), so it appears on the native path only — the legacy `chat.update`
fallback omits it.
### Native "is thinking…" status (everywhere)
While the agent runs, the bot shows Slack's **native** loading status
(`assistant.threads.setStatus`: "is thinking…") on every thread-anchored reply —
channel @-mentions, threads it owns, DMs, and the assistant pane. Slack now
accepts this method with the ordinary **`chat:write`** scope (no `assistant:write`
needed just for the loading state), so it works for channel-based apps too. The
status auto-clears when the reply streams in. Tool progress is surfaced per
surface: the pane uses live composer status ("is using \`tool\`…"); elsewhere it
uses the native `task_update` timeline (or `:wrench:` rows on older workspaces).
Set `assistant: false` to opt out of the status (and pane) entirely.
### Assistant pane (agent-native, default-on)
When the Slack app has the **Agents & AI Apps** toggle (an `assistant_view`
manifest block + the `assistant:write` scope and `assistant_thread_*` events),
the adapter activates Slack's assistant pane with **zero config**:
- Opening the pane posts a greeting + tappable prompt chips, and each pane
conversation is its own thread (replies stay in-thread).
- While the agent runs, native composer status is shown (see above), with
"is using \`tool\`…" per tool call.
- The pane thread is auto-titled from the first message.
Customize via the `assistant` option, or set `assistant: false` to disable pane
handling entirely. Apps **without** the toggle behave exactly as before — the
pane machinery lies dormant.
```ts
slack({
botToken,
appToken,
assistant: {
greeting: "Hi! I can triage issues, search docs, and more.",
suggestedPrompts: [
{ title: "Triage my open issues", message: "Triage my open issues" },
],
},
});
// Dynamic behavior when a user opens the pane (layers on top of the defaults):
bot.onThreadStarted(async ({ thread, user }) => {
await thread.setSuggestedPrompts(promptsFor(user));
// await thread.setTitle(...) is also available
});
```
### Interactions (ack-first)
Every Slack `block_actions` click is acked immediately (within the **≤3s**
deadline, `ackDeadlineMs = 3000`), then `decodeInteraction` extracts the
opaque minted id (`ck:…`), any tiny `bind()` value, and the message ref, and
hands an `InteractionEvent` to the engine. The token carries only the opaque
id — no props or secrets. Unrelated clicks decode to events the bot
harmlessly ignores.
### Human-in-the-loop
Use `thread.awaitChoice(<Picker .../>)` to post an interactive message and
block until a click resolves it; the resolved value is the clicked control's
value. Agent interrupts (`on_interrupt`) are captured by the run renderer and
dispatched to your `onInterrupt` handler, which posts a picker; the click
resumes the agent via `thread.resume(value)`.
### Sender-profile resolution & file download
The adapter resolves each turn's Slack user id to a richer `PlatformUser`
(`{ id, name?, email? }`), cached per id. Inbound files can be downloaded and
delivered to the agent as multimodal content parts (`buildFileContentParts`);
a tool can post a file back out via `thread.postFile(...)`.
### Built-ins
- `defaultSlackTools` — ships `lookup_slack_user` so the agent can resolve a
name/handle/email to a `<@USERID>` mention. Spread into `tools`.
- `defaultSlackContext` — tagging procedure, Markdown-vs-mrkdwn guidance, and
the Slack thread/DM conversation model. Spread into `context`.
## Tool context
There is no Slack-specific tool context. Tools receive the single shared
`BotToolContext` from `@copilotkit/channels` (`{ thread, message?, user?, signal?,
platform }`) and reach Slack power only through capability-gated `thread`
methods, which this adapter backs:
- `thread.getMessages()` — the current thread's messages (via
`conversations.replies`), each a `ThreadMessage` (`{ user?, text, ts?,
isBot? }`).
- `thread.lookupUser(query)` — resolve a name/handle/email to a `PlatformUser`.
- `thread.postFile({ bytes, filename, title?, altText? })` — upload a file
back into the thread (`files.uploadV2`).
This keeps tools portable: define them with `defineBotTool({...})` and they
work against any adapter that advertises the same capabilities.
## Running the demo
This package is the **library**. A runnable end-to-end demo wiring all of the
above against a real workspace lives in
[`examples/slack`](../../examples/slack).
## Slash commands
The adapter forwards every slash command Slack delivers to the engine, which
routes it to the matching `bot.onCommand` handler (and ignores unregistered
ones). Register handlers on the engine — see
[`@copilotkit/channels`](../channels/README.md):
```ts
bot.onCommand({
name: "triage",
description: "Summarize the thread and propose issues.",
async handler({ thread, text, user }) {
await thread.runAgent({ prompt: `Triage: ${text}` });
},
});
```
**You must also declare each command in the Slack app config** ("Slash
Commands" / app manifest) with the same name — Slack won't deliver an
unregistered command, even over Socket Mode. Args arrive as free text
(`ctx.text`); the optional `options` schema is for surfaces with native
structured args (e.g. Discord) and is unused on Slack. The adapter does not
implement `registerCommands`, so the engine skips it (Slack matches commands
dynamically rather than registering them up front).
## OAuth bot scopes
The following bot token scopes are required or relevant depending on the
features your app uses:
| Scope | Required for |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `chat:write` | Posting messages, streaming, ephemeral messages (`chat.postEphemeral`), and opening modals (`views.open`) — all share this single scope. |
| `reactions:read` | Reading reactions; subscribe to `reaction_added` / `reaction_removed` events in the app manifest to receive them. |
| `reactions:write` | Adding or removing reactions via `reactions.add` / `reactions.remove`. |
| `assistant:write` | Native streaming `task_update` tool-timeline chunks and the assistant pane. (The "is thinking…" status works with `chat:write` alone.) |
| `files:write` | Uploading files via `thread.postFile()`. |
| `users:read` | Resolving Slack user profiles (name, email) via `users.info`. |
| `users:read.email` | Resolving user email addresses. |
| `channels:history` | Reading channel thread messages via `conversations.replies`. |
| `groups:history` | Reading private-channel thread messages via `conversations.replies`. |
| `im:history` | Reading DM thread messages via `conversations.replies`. |
| `mpim:history` | Reading group-DM thread messages via `conversations.replies`. |
### Notes
- **Modals** (`views.open`, `view_submission`, `view_closed`): handled via
`chat:write` — no additional scope is needed.
- **Ephemeral messages** (`chat.postEphemeral`): covered by `chat:write`.
- **Reactions** (`reactions:read` / `reactions:write`): these scopes alone
are not enough — you must also subscribe to the `reaction_added` and
`reaction_removed` events in the Slack app manifest so that Slack delivers
the events to your bot.
## What's NOT in v1
- OAuth / multi-workspace install (single bot token only)
- Durable (Redis/DB) `ActionStore` — in-memory only; actions expire on
restart
- Proactive posting (bot replies only to turns it's part of)
## Exports
`slack`, `SlackAdapter`, `SlackAdapterOptions`, `SlackAssistantOptions`,
`SlackRespondToOptions`;
`createRunRenderer`; `decodeInteraction`, `conversationKeyOf`; `renderBlockKit`,
`renderSlackMessage`, `SLACK_LIMITS`; `defaultSlackTools`,
`lookupSlackUserTool`, `defaultSlackContext` (+ the individual context
entries); `markdownToMrkdwn`; and the
preserved mechanics (`SlackConversationStore`, `MessageStream`,
`ChunkedMessageStream`, `NativeMessageStream`, `attachSlackListener`,
`attachAssistant`, `SanitizingHttpAgent`, `buildFileContentParts`,
`autoCloseOpenMarkdown`, and supporting types).