Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/bright-agents-stop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@chat-adapter/slack": minor
"chat": minor
---

Add Slack Agent Sessions lifecycle support, native stop cancellation through `thread.signal`, automatic session titles, and session stop/title-change events while preserving the legacy `assistant_view` compatibility path.
89 changes: 82 additions & 7 deletions apps/docs/content/adapters/official/slack.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ packageName: "@chat-adapter/slack"
slug: slack
type: platform
logo: slack
tagline: Build bots for Slack workspaces with full support for threads, reactions, native streaming, scheduled messages, modals, slash commands, and the Assistants API.
tagline: Build bots for Slack workspaces with full support for threads, reactions, Agent Sessions, native streaming, scheduled messages, modals, and slash commands.
beta: true
features:
postMessage: yes
Expand Down Expand Up @@ -142,6 +142,12 @@ bot.onNewMention(async (thread, message) => {
description:
"Enable the Agent messaging experience (agent_view manifest mode).",
},
sessionTitle: {
type: "boolean | ((context) => string | Promise<string | null> | null)",
default: "true when agentView is enabled",
description:
"Automatically title new agent sessions from the root user message, or customize titles with a resolver.",
},
suggestedPrompts: {
type: "SlackSuggestedPrompts",
description:
Expand All @@ -150,7 +156,7 @@ bot.onNewMention(async (thread, message) => {
loadingMessages: {
type: "string[]",
description:
"Default rotating status strings for the assistant thinking indicator, used by startTyping and setAssistantStatus when no explicit status is passed.",
"Legacy assistant_view rotating status strings. Agent Sessions use Slack's standard Working state.",
},
nativeStreaming: {
type: "boolean",
Expand Down Expand Up @@ -614,7 +620,7 @@ The package still installs the full Slack adapter dependencies. The subpaths kee

### Agents

Everything for building an AI agent on Slack: the Agent messaging experience (`agent_view`), the Assistants API (suggested prompts, status, titles), native streaming, and feedback buttons.
Everything for building an AI agent on Slack: the Agent messaging experience (`agent_view`), Agent Sessions, suggested prompts, native streaming, and feedback buttons.

#### Agent messaging experience

Expand All @@ -624,13 +630,18 @@ Slack's Agent messaging experience (`agent_view` manifest mode) supersedes the o
const slack = createSlackAdapter({ agentView: true });
```

Slack deprecated `assistant_view` on August 20, 2026 and will retire it in
February 2027. Chat SDK keeps the legacy path available when `agentView` is
false, but new and migrated apps should use Agent messaging now.

With `agentView: true`:

- `onAppHomeOpened` is the DM-open signal (Slack no longer signals DM-open via `assistant_thread_started` under `agent_view`), and it fires regardless of the opened tab — branch on `event.tab` (`"home"` vs `"messages"`) if you also publish a Home view.
- `onAppContextChanged` reports the user's active view (see [Handling active-view context](/docs/handling-events#handling-active-view-context-agent-messaging)).
- `getAppContext(message)` returns the folded active-view context on a DM message.
- `setSuggestedPrompts(channelId, undefined, prompts)` may omit the thread reference — prompts sit at the top of the agent conversation. A `suggestedPrompts` config entry is applied automatically on every Messages-tab open.
- DM messages are threaded per Slack's model (each user message is a thread root). Threads returned by `openDM()` keep working: when the conversation-scoped thread is subscribed, incoming top-level DM messages route to it, so `onSubscribedMessage` and per-thread state behave as before.
- New sessions are titled from the first line of the root message by default. Set `sessionTitle: false` to disable this, or pass a resolver to customize it.

<Callout type="warn">
Because bot replies are threaded under each user message, channel-level history (`channel.messages`, `conversations.history`) only returns the user's side of a DM conversation. If you build AI conversation history for DMs, use [transcripts](/docs/conversation-history) (which record both roles across thread IDs) instead of channel history — otherwise the model never sees its own previous replies.
Expand All @@ -643,15 +654,74 @@ oauth_config:
scopes:
bot:
- assistant:write
- chat:write

settings:
event_subscriptions:
bot_events:
- app_home_opened
- app_context_changed
- agent_session_stopped
- agent_session_title_changed
```

#### Agent Sessions API

With `agentView: true`, `startTyping()` transitions the session to
`processing`. Slack shows its standard Working indicator and a native stop
button. Chat SDK returns the session to `active` after posts and streams; use
`setSessionStatus()` directly for `suspended` or `closed` states.

```typescript
await thread.startTyping();

const result = await agent.stream({
prompt: message.text,
abortSignal: thread.signal,
});
await thread.post(result.fullStream);
```

Always pass `thread.signal` to model APIs. When the user clicks Slack's stop
button, Chat SDK aborts that signal locally and through the configured shared
state adapter, stops rendering the stream, and transitions the session out of
`processing`.

```typescript
bot.onAgentSessionStopped(async (event) => {
await releaseExternalResources(event.threadId);
});

bot.onAgentSessionTitleChanged(async (event) => {
await syncTitle(event.threadId, event.title);
});
```

#### Slack Assistants API
The `SlackAdapter` exposes:

| Method | Description |
|--------|-------------|
| `setSessionStatus(channelId, threadTs, status)` | Set `processing`, `active`, `suspended`, or `closed` |
| `setAssistantTitle(channelId, threadTs, title)` | Rename the agent session |
| `setSuggestedPrompts(channelId, threadTs, prompts, title?)` | Show prompt suggestions |
| `publishHomeView(userId, view)` | Publish a Home tab view |
| `startTyping(threadId)` | Set the agent session to `processing` |

`setAssistantStatus` and `setAssistantTitle` remain compatibility methods:
under `agentView`, they map to `agents.sessions.*`; under legacy
`assistant_view`, they call `assistant.threads.*`. Custom status text and
`loadingMessages` only apply to the legacy experience.

Customize automatic titles with `sessionTitle`:

```typescript
const slack = createSlackAdapter({
agentView: true,
sessionTitle: ({ text }) => text.split("\n", 1)[0]?.slice(0, 80) ?? null,
});
```

#### Legacy Slack Assistants API

The adapter supports Slack's [Assistants API](https://api.slack.com/docs/apps/ai). Register handlers on the `Chat` instance:

Expand Down Expand Up @@ -698,7 +768,7 @@ const slack = createSlackAdapter({
});
```

`loadingMessages` becomes the default for `startTyping(threadId)` and `setAssistantStatus(...)` when no explicit status/messages are passed.
`loadingMessages` becomes the default for `startTyping(threadId)` and `setAssistantStatus(...)` in legacy `assistant_view` when no explicit status/messages are passed.

The `SlackAdapter` exposes:

Expand Down Expand Up @@ -785,6 +855,8 @@ display_information:
description: A bot built with chat-sdk

features:
agent_view:
agent_description: A bot built with Chat SDK
bot_user:
display_name: My Bot
always_online: true
Expand All @@ -793,6 +865,7 @@ oauth_config:
scopes:
bot:
- app_mentions:read
- assistant:write
- channels:history
- channels:read
- chat:write
Expand All @@ -816,8 +889,10 @@ settings:
- message.im
- message.mpim
- member_joined_channel
- assistant_thread_started
- assistant_thread_context_changed
- app_home_opened
- app_context_changed
- agent_session_stopped
- agent_session_title_changed
interactivity:
is_enabled: true
request_url: https://your-domain.com/api/webhooks/slack
Expand Down
43 changes: 43 additions & 0 deletions apps/docs/content/docs/api/chat.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,36 @@ bot.onAssistantContextChanged(async (event) => {

The event shape is identical to `onAssistantThreadStarted`.

### onAgentSessionStopped

Fires after a user clicks Slack's native agent-session stop button. Chat SDK
aborts the active turn and moves the session out of `processing` before this
handler runs.

```typescript
bot.onAgentSessionStopped(async (event) => {
await releaseExternalResources(event.threadId);
});
```

The event contains `threadId`, `channelId`, `threadTs`,
`streamingMessageTs`, `userId`, and `adapter`. `streamingMessageTs` lists the
streaming messages Slack stopped and can be empty when no stream was active.

### onAgentSessionTitleChanged

Fires when a user renames a Slack agent session.

```typescript
bot.onAgentSessionTitleChanged(async (event) => {
await syncTitle(event.threadId, event.title);
});
```

The event contains `title`, `previousTitle`, `threadId`, `channelId`,
`threadTs`, `userId`, and `adapter`. `previousTitle` is omitted when the
session did not have a title before the change.

### onAppHomeOpened

Fires when a user opens the bot's Home tab in Slack. Use this to publish a dynamic Home tab view.
Expand Down Expand Up @@ -485,6 +515,19 @@ bot.onAppHomeOpened(async (event) => {

## Utility methods

### abortTurn

Abort the active handler turn for a thread. Chat SDK signals work in this
process immediately and publishes the cancellation through the configured
state adapter for another serverless instance to observe.

```typescript
await bot.abortTurn(thread.id);
```

Platform adapters normally invoke this for native cancellation events. Model
and tool calls must receive `thread.signal` to stop their own upstream work.

### webhooks

Type-safe webhook handlers keyed by adapter name. Pass these to your HTTP route handler.
Expand Down
24 changes: 23 additions & 1 deletion apps/docs/content/docs/api/thread.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,10 @@ await thread.setState({ aiMode: false }, { replace: true });

## startTyping

Show a typing indicator in the thread. No-op on platforms that don't support it. On Slack, you can pass an optional `status` string to show a custom loading message (requires `assistant:write` scope).
Show a typing indicator in the thread. No-op on platforms that don't support
it. With Slack Agent messaging, this sets the session to `processing` and
shows Slack's standard Working state plus a native stop button. Custom status
text only applies to legacy `assistant_view`.

```typescript
await thread.startTyping();
Expand All @@ -203,6 +206,25 @@ await thread.startTyping();
await thread.startTyping("Searching documents...");
```

## signal

An `AbortSignal` for the active turn. Slack aborts it when the user clicks the
native agent-session stop button, including when the stop webhook reaches a
different serverless instance sharing the same state adapter.

Pass it to model and tool APIs so cancellation stops upstream work:

```typescript
const result = await agent.stream({
prompt: message.text,
abortSignal: thread.signal,
});
await thread.post(result.fullStream);
```

Threads created outside an incoming handler expose a signal that remains
unaborted.

## markAsRead

Mark an inbound message as read. WhatsApp, Messenger, and XChat support this capability. Other adapters throw `NotImplementedError`.
Expand Down
26 changes: 25 additions & 1 deletion apps/docs/content/docs/handling-events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,30 @@ For button clicks, slash commands, and modal forms, see the dedicated guides:

These handlers are specific to the Slack platform and require the Slack adapter.

### Handling agent session stop and title changes

`onAgentSessionStopped` fires when a user clicks Slack's native stop button.
Chat SDK aborts the active `thread.signal` and transitions the session out of
`processing` before invoking your handler.

```typescript title="lib/bot.ts" lineNumbers
bot.onAgentSessionStopped(async (event) => {
await releaseExternalResources(event.threadId);
});
```

`onAgentSessionTitleChanged` fires when a user renames a session in Slack:

```typescript title="lib/bot.ts" lineNumbers
bot.onAgentSessionTitleChanged(async (event) => {
await syncTitle(event.threadId, event.title);
});
```

Subscribe to `agent_session_stopped` and `agent_session_title_changed` in the
Slack app manifest. Model calls should receive `thread.signal` so stop also
cancels upstream generation.

### Handling assistant threads

`onAssistantThreadStarted` fires when a user opens a new assistant thread in Slack. Use it with the [Slack Assistants API](/adapters/official/slack#slack-assistants-api) to set suggested prompts and status indicators.
Expand Down Expand Up @@ -387,7 +411,7 @@ The `event` object includes:
```typescript title="lib/bot.ts" lineNumbers
bot.onAssistantContextChanged(async (event) => {
const slack = bot.getAdapter("slack") as SlackAdapter;
await slack.setStatus(event.channelId, event.threadTs, "Updating context...");
await slack.setAssistantStatus(event.channelId, event.threadTs, "Updating context...");

// Update prompts based on new context
const channelName = event.context.channelId ?? "general";
Expand Down
21 changes: 20 additions & 1 deletion apps/docs/content/docs/streaming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,19 @@ const agent = new ToolLoopAgent({
});

bot.onNewMention(async (thread, message) => {
const result = await agent.stream({ prompt: message.text });
const result = await agent.stream({
prompt: message.text,
abortSignal: thread.signal,
});
await thread.post(result.fullStream);
});
```

Passing `thread.signal` lets platform cancellation stop model generation. In
Slack Agent messaging, clicking the native stop button aborts this signal even
when the stop webhook is handled by another serverless instance using the same
state adapter.

### Why `fullStream` over `textStream`?

When AI SDK agents make tool calls between text steps, `textStream` concatenates all text without separators — `"hello.how are you?"` instead of `"hello.\n\nhow are you?"`. The `fullStream` contains explicit `finish-step` events that Chat SDK uses to inject paragraph breaks between steps automatically.
Expand All @@ -44,6 +52,17 @@ await thread.post(result.fullStream);
await thread.post(result.textStream);
```

For a human-in-the-loop turn, wrap the stream in `StreamingPlan` and leave the
Slack Agent Session suspended after the final chunk:

```typescript
await thread.post(
new StreamingPlan(result.fullStream, {
sessionStatus: "suspended",
})
);
```

## Custom streams

Any async iterable works:
Expand Down
1 change: 1 addition & 0 deletions examples/nextjs-chat/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.env*.local
.env*
/.swc
4 changes: 3 additions & 1 deletion examples/nextjs-chat/slack-manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ oauth_config:
- reactions:write
# User info for display names
- users:read
# Agent experience (suggested prompts, thinking status) — uncomment
# Agent experience (suggested prompts and Agent Sessions) — uncomment
# together with the agent_view / event blocks below.
# - assistant:write

Expand All @@ -52,6 +52,8 @@ settings:
# Agent experience events — uncomment together with agent_view above.
# - app_home_opened
# - app_context_changed
# - agent_session_stopped
# - agent_session_title_changed
interactivity:
is_enabled: false
org_deploy_enabled: false
Expand Down
3 changes: 2 additions & 1 deletion examples/nextjs-chat/src/lib/adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ const DISCORD_METHODS = [
// Suggested prompts are pinned automatically when an assistant/agent thread
// opens; the resolver tailors them to the user's active view (agent_view
// folds what the user is looking at into the event as `entities`).
// `loadingMessages` rotate in the thinking indicator while the bot works.
// `loadingMessages` rotate only in legacy assistant_view; agent_view uses
// Slack's standard Agent Sessions working state and native stop button.
// Set SLACK_AGENT_VIEW=true when your manifest uses `agent_view`.
// Set SLACK_NATIVE_STREAMING=false to compare Slack's native streaming API
// (chat.startStream/appendStream/stopStream, the default) against the
Expand Down
Loading