chat 4.25.0 → 4.27.0
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/dist/{chunk-OPV5U4WG.js → chunk-AN7MRAVW.js} +39 -0
- package/dist/index.d.ts +235 -6
- package/dist/index.js +353 -76
- package/dist/{jsx-runtime-DxATbnrP.d.ts → jsx-runtime-Co9uV6l7.d.ts} +39 -5
- package/dist/jsx-runtime.d.ts +1 -1
- package/dist/jsx-runtime.js +1 -1
- package/docs/adapters.mdx +30 -30
- package/docs/api/cards.mdx +5 -0
- package/docs/api/chat.mdx +95 -1
- package/docs/api/message.mdx +5 -1
- package/docs/api/modals.mdx +1 -1
- package/docs/api/thread.mdx +23 -1
- package/docs/cards.mdx +6 -0
- package/docs/contributing/publishing.mdx +33 -0
- package/docs/files.mdx +1 -0
- package/docs/getting-started.mdx +2 -12
- package/docs/meta.json +0 -2
- package/docs/modals.mdx +74 -2
- package/docs/state.mdx +1 -1
- package/docs/streaming.mdx +13 -5
- package/docs/threads-messages-channels.mdx +34 -0
- package/docs/usage.mdx +2 -2
- package/package.json +3 -2
- package/resources/guides/create-a-discord-support-bot-with-nuxt-and-redis.md +180 -0
- package/resources/guides/how-to-build-a-slack-bot-with-next-js-and-redis.md +134 -0
- package/resources/guides/how-to-build-an-ai-agent-for-slack-with-chat-sdk-and-ai-sdk.md +220 -0
- package/resources/guides/run-and-track-deploys-from-slack.md +270 -0
- package/resources/guides/ship-a-github-code-review-bot-with-hono-and-redis.md +147 -0
- package/resources/guides/triage-form-submissions-with-chat-sdk.md +178 -0
- package/resources/templates.json +19 -0
- package/docs/adapters/whatsapp.mdx +0 -222
- package/docs/guides/code-review-hono.mdx +0 -241
- package/docs/guides/discord-nuxt.mdx +0 -227
- package/docs/guides/durable-chat-sessions-nextjs.mdx +0 -331
- package/docs/guides/meta.json +0 -10
- package/docs/guides/scheduled-posts-neon.mdx +0 -447
- package/docs/guides/slack-nextjs.mdx +0 -234
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Discord support bot with Nuxt and Redis
|
|
3
|
-
description: This guide walks through building a Discord support bot with Nuxt, covering project setup, Discord app configuration, Gateway forwarding, AI-powered responses, and deployment.
|
|
4
|
-
type: guide
|
|
5
|
-
prerequisites: []
|
|
6
|
-
related:
|
|
7
|
-
- /adapters/discord
|
|
8
|
-
- /docs/cards
|
|
9
|
-
- /docs/actions
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## Prerequisites
|
|
13
|
-
|
|
14
|
-
- Node.js 18+
|
|
15
|
-
- [pnpm](https://pnpm.io) (or npm/yarn)
|
|
16
|
-
- A Discord server where you have admin access
|
|
17
|
-
- A Redis instance for state management
|
|
18
|
-
|
|
19
|
-
## Create a Nuxt app
|
|
20
|
-
|
|
21
|
-
Scaffold a new Nuxt project and install Chat SDK dependencies:
|
|
22
|
-
|
|
23
|
-
```sh title="Terminal"
|
|
24
|
-
npx nuxi@latest init my-discord-bot
|
|
25
|
-
cd my-discord-bot
|
|
26
|
-
pnpm add chat @chat-adapter/discord @chat-adapter/state-redis ai @ai-sdk/anthropic
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Create a Discord app
|
|
30
|
-
|
|
31
|
-
1. Go to [discord.com/developers/applications](https://discord.com/developers/applications)
|
|
32
|
-
2. Click **New Application**, give it a name, and click **Create**
|
|
33
|
-
3. Go to **Bot** in the sidebar and click **Reset Token** — copy the token, you'll need this as `DISCORD_BOT_TOKEN`
|
|
34
|
-
4. Under **Privileged Gateway Intents**, enable **Message Content Intent**
|
|
35
|
-
5. Go to **General Information** and copy the **Application ID** and **Public Key** — you'll need these as `DISCORD_APPLICATION_ID` and `DISCORD_PUBLIC_KEY`
|
|
36
|
-
|
|
37
|
-
### Set up the Interactions endpoint
|
|
38
|
-
|
|
39
|
-
1. In **General Information**, set the **Interactions Endpoint URL** to `https://your-domain.com/api/webhooks/discord`
|
|
40
|
-
2. Discord will send a PING to verify the endpoint — you'll need to deploy first or use a tunnel
|
|
41
|
-
|
|
42
|
-
### Invite the bot to your server
|
|
43
|
-
|
|
44
|
-
1. Go to **OAuth2** in the sidebar
|
|
45
|
-
2. Under **OAuth2 URL Generator**, select the `bot` scope
|
|
46
|
-
3. Under **Bot Permissions**, select:
|
|
47
|
-
- Send Messages
|
|
48
|
-
- Create Public Threads
|
|
49
|
-
- Send Messages in Threads
|
|
50
|
-
- Read Message History
|
|
51
|
-
- Add Reactions
|
|
52
|
-
- Use Slash Commands
|
|
53
|
-
4. Copy the generated URL and open it in your browser to invite the bot
|
|
54
|
-
|
|
55
|
-
## Configure environment variables
|
|
56
|
-
|
|
57
|
-
Create a `.env` file in your project root:
|
|
58
|
-
|
|
59
|
-
```bash title=".env"
|
|
60
|
-
DISCORD_BOT_TOKEN=your_bot_token
|
|
61
|
-
DISCORD_PUBLIC_KEY=your_public_key
|
|
62
|
-
DISCORD_APPLICATION_ID=your_application_id
|
|
63
|
-
REDIS_URL=redis://localhost:6379
|
|
64
|
-
ANTHROPIC_API_KEY=your_anthropic_api_key
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
## Create the bot
|
|
68
|
-
|
|
69
|
-
Create `server/lib/bot.ts` with a `Chat` instance configured with the Discord adapter. This bot uses AI SDK to answer support questions:
|
|
70
|
-
|
|
71
|
-
```typescript title="server/lib/bot.tsx" lineNumbers
|
|
72
|
-
import { Chat, Card, CardText as Text, Actions, Button, Divider } from "chat";
|
|
73
|
-
import { createDiscordAdapter } from "@chat-adapter/discord";
|
|
74
|
-
import { createRedisState } from "@chat-adapter/state-redis";
|
|
75
|
-
import { generateText } from "ai";
|
|
76
|
-
import { anthropic } from "@ai-sdk/anthropic";
|
|
77
|
-
|
|
78
|
-
export const bot = new Chat({
|
|
79
|
-
userName: "support-bot",
|
|
80
|
-
adapters: {
|
|
81
|
-
discord: createDiscordAdapter(),
|
|
82
|
-
},
|
|
83
|
-
state: createRedisState(),
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
bot.onNewMention(async (thread) => {
|
|
87
|
-
await thread.subscribe();
|
|
88
|
-
await thread.post(
|
|
89
|
-
<Card title="Support">
|
|
90
|
-
<Text>Hey! I'm here to help. Ask your question in this thread and I'll do my best to answer it.</Text>
|
|
91
|
-
<Divider />
|
|
92
|
-
<Actions>
|
|
93
|
-
<Button id="escalate" style="danger">Escalate to Human</Button>
|
|
94
|
-
</Actions>
|
|
95
|
-
</Card>
|
|
96
|
-
);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
bot.onSubscribedMessage(async (thread, message) => {
|
|
100
|
-
await thread.startTyping();
|
|
101
|
-
|
|
102
|
-
const { text } = await generateText({
|
|
103
|
-
model: anthropic("claude-sonnet-4-5-20250514"),
|
|
104
|
-
system: "You are a friendly support bot. Answer questions concisely. If you don't know the answer, say so and suggest the user click 'Escalate to Human'.",
|
|
105
|
-
prompt: message.text,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
await thread.post(text);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
bot.onAction("escalate", async (event) => {
|
|
112
|
-
await event.thread.post(
|
|
113
|
-
`${event.user.fullName} requested human support. A team member will follow up shortly.`
|
|
114
|
-
);
|
|
115
|
-
});
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
<Callout type="info">
|
|
119
|
-
The file extension must be `.tsx` (not `.ts`) when using JSX components like `Card` and `Button`. Make sure your `tsconfig.json` has `"jsx": "react-jsx"` and `"jsxImportSource": "chat"`.
|
|
120
|
-
</Callout>
|
|
121
|
-
|
|
122
|
-
`onNewMention` fires when a user @mentions the bot. Calling `thread.subscribe()` tells the SDK to track that thread, so subsequent messages trigger `onSubscribedMessage` where AI SDK generates a response.
|
|
123
|
-
|
|
124
|
-
## Create the webhook route
|
|
125
|
-
|
|
126
|
-
Create a server route that handles incoming Discord webhooks:
|
|
127
|
-
|
|
128
|
-
```typescript title="server/api/webhooks/[platform].post.ts" lineNumbers
|
|
129
|
-
import { bot } from "../lib/bot";
|
|
130
|
-
|
|
131
|
-
type Platform = keyof typeof bot.webhooks;
|
|
132
|
-
|
|
133
|
-
export default defineEventHandler(async (event) => {
|
|
134
|
-
const platform = getRouterParam(event, "platform") as Platform;
|
|
135
|
-
|
|
136
|
-
const handler = bot.webhooks[platform];
|
|
137
|
-
if (!handler) {
|
|
138
|
-
throw createError({ statusCode: 404, message: `Unknown platform: ${platform}` });
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const request = toWebRequest(event);
|
|
142
|
-
|
|
143
|
-
return handler(request, {
|
|
144
|
-
waitUntil: (task) => event.waitUntil(task),
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
This creates a `POST /api/webhooks/discord` endpoint. The `waitUntil` option ensures message processing completes after the HTTP response is sent.
|
|
150
|
-
|
|
151
|
-
## Set up the Gateway forwarder
|
|
152
|
-
|
|
153
|
-
Discord doesn't push messages to webhooks like Slack does. Instead, messages arrive through the Gateway WebSocket. The Discord adapter includes a built-in Gateway listener that connects to the WebSocket and forwards events to your webhook endpoint.
|
|
154
|
-
|
|
155
|
-
Create a route that starts the Gateway listener:
|
|
156
|
-
|
|
157
|
-
```typescript title="server/api/discord/gateway.get.ts" lineNumbers
|
|
158
|
-
import { bot } from "../../lib/bot";
|
|
159
|
-
|
|
160
|
-
export default defineEventHandler(async (event) => {
|
|
161
|
-
await bot.initialize();
|
|
162
|
-
|
|
163
|
-
const discord = bot.getAdapter("discord");
|
|
164
|
-
if (!discord) {
|
|
165
|
-
throw createError({ statusCode: 404, message: "Discord adapter not configured" });
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const baseUrl = process.env.NUXT_PUBLIC_SITE_URL || "http://localhost:3000";
|
|
169
|
-
const webhookUrl = `${baseUrl}/api/webhooks/discord`;
|
|
170
|
-
|
|
171
|
-
const durationMs = 10 * 60 * 1000; // 10 minutes
|
|
172
|
-
|
|
173
|
-
return discord.startGatewayListener(
|
|
174
|
-
{ waitUntil: (task: Promise<unknown>) => event.waitUntil(task) },
|
|
175
|
-
durationMs,
|
|
176
|
-
undefined,
|
|
177
|
-
webhookUrl,
|
|
178
|
-
);
|
|
179
|
-
});
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
The Gateway listener connects to Discord's WebSocket, receives messages, and forwards them to your webhook endpoint for processing. In production, you'll want a cron job to restart it periodically.
|
|
183
|
-
|
|
184
|
-
## Test locally
|
|
185
|
-
|
|
186
|
-
1. Start your development server (`pnpm dev`)
|
|
187
|
-
2. Trigger the Gateway listener by visiting `http://localhost:3000/api/discord/gateway` in your browser
|
|
188
|
-
3. Expose your server with a tunnel (e.g. `ngrok http 3000`)
|
|
189
|
-
4. Update the **Interactions Endpoint URL** in your Discord app settings to your tunnel URL (e.g. `https://abc123.ngrok.io/api/webhooks/discord`)
|
|
190
|
-
5. @mention the bot in your Discord server — it should respond with a support card
|
|
191
|
-
6. Reply in the thread — AI SDK should generate a response
|
|
192
|
-
7. Click **Escalate to Human** — the bot should post an escalation message
|
|
193
|
-
|
|
194
|
-
## Add a cron job for production
|
|
195
|
-
|
|
196
|
-
The Gateway listener runs for a fixed duration. In production, set up a cron job to restart it automatically. If you're deploying to Vercel, add a `vercel.json`:
|
|
197
|
-
|
|
198
|
-
```json title="vercel.json"
|
|
199
|
-
{
|
|
200
|
-
"crons": [
|
|
201
|
-
{
|
|
202
|
-
"path": "/api/discord/gateway",
|
|
203
|
-
"schedule": "*/9 * * * *"
|
|
204
|
-
}
|
|
205
|
-
]
|
|
206
|
-
}
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
This restarts the Gateway listener every 9 minutes, ensuring continuous connectivity. Protect the endpoint with a `CRON_SECRET` environment variable in production.
|
|
210
|
-
|
|
211
|
-
## Deploy to Vercel
|
|
212
|
-
|
|
213
|
-
Deploy your bot to Vercel:
|
|
214
|
-
|
|
215
|
-
```sh title="Terminal"
|
|
216
|
-
vercel deploy
|
|
217
|
-
```
|
|
218
|
-
|
|
219
|
-
After deployment, set your environment variables in the Vercel dashboard (`DISCORD_BOT_TOKEN`, `DISCORD_PUBLIC_KEY`, `DISCORD_APPLICATION_ID`, `REDIS_URL`, `ANTHROPIC_API_KEY`). Update the **Interactions Endpoint URL** in your Discord app settings to your production URL.
|
|
220
|
-
|
|
221
|
-
## Next steps
|
|
222
|
-
|
|
223
|
-
- [Cards](/docs/cards) — Build rich interactive messages with buttons, fields, and selects
|
|
224
|
-
- [Actions](/docs/actions) — Handle button clicks, select menus, and other interactions
|
|
225
|
-
- [Streaming](/docs/streaming) — Stream AI-generated responses to chat
|
|
226
|
-
- [Discord adapter](/adapters/discord) — Full configuration reference and Gateway setup
|
|
227
|
-
- [State Adapters](/docs/state) — PostgreSQL, ioredis, and other state adapter options
|
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Durable chat sessions with Next.js, Workflow, and Redis
|
|
3
|
-
description: This guide walks through combining Chat SDK and Workflow so a chat thread can survive restarts, wait for follow-up messages, and keep its session state in Redis.
|
|
4
|
-
type: guide
|
|
5
|
-
prerequisites: []
|
|
6
|
-
related:
|
|
7
|
-
- /docs/guides/slack-nextjs
|
|
8
|
-
- /docs/handling-events
|
|
9
|
-
- /docs/streaming
|
|
10
|
-
- /docs/api/thread
|
|
11
|
-
- /docs/api/chat
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
Chat SDK and Workflow solve different parts of the same problem.
|
|
15
|
-
|
|
16
|
-
Chat SDK normalizes incoming platform events into `thread` and `message` objects and gives you a consistent way to reply. Workflow gives you durable execution so a session can wait for the next turn without holding a request open or losing state on restart.
|
|
17
|
-
|
|
18
|
-
This guide uses Slack and Next.js for a concrete example, but the same pattern works with any Chat SDK adapter.
|
|
19
|
-
|
|
20
|
-
## Prerequisites
|
|
21
|
-
|
|
22
|
-
- Node.js 18+
|
|
23
|
-
- [pnpm](https://pnpm.io) (or npm/yarn)
|
|
24
|
-
- A Next.js App Router project
|
|
25
|
-
- A Slack workspace where you can install apps
|
|
26
|
-
- A Redis instance for Chat SDK state
|
|
27
|
-
|
|
28
|
-
<Callout type="info">
|
|
29
|
-
If you still need the Slack app manifest and webhook setup, start with [Slack bot with Next.js and Redis](/docs/guides/slack-nextjs), then come back here to add Workflow.
|
|
30
|
-
</Callout>
|
|
31
|
-
|
|
32
|
-
## Install the dependencies
|
|
33
|
-
|
|
34
|
-
Install Chat SDK, the Slack adapter, Redis state, and Workflow:
|
|
35
|
-
|
|
36
|
-
```sh title="Terminal"
|
|
37
|
-
pnpm add chat @chat-adapter/slack @chat-adapter/state-redis workflow
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## Enable Workflow in Next.js
|
|
41
|
-
|
|
42
|
-
Wrap your Next.js config with `withWorkflow()` so `"use workflow"` and `"use step"` directives are compiled correctly:
|
|
43
|
-
|
|
44
|
-
```typescript title="next.config.ts" lineNumbers
|
|
45
|
-
import { withWorkflow } from "workflow/next";
|
|
46
|
-
import type { NextConfig } from "next";
|
|
47
|
-
|
|
48
|
-
const nextConfig: NextConfig = {
|
|
49
|
-
// ...your existing config
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export default withWorkflow(nextConfig);
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
<Callout type="info">
|
|
56
|
-
If your app uses `proxy.ts`, exclude `.well-known/workflow/` from the matcher so Workflow's internal routes are not intercepted.
|
|
57
|
-
</Callout>
|
|
58
|
-
|
|
59
|
-
## Create the Chat instance
|
|
60
|
-
|
|
61
|
-
Create a bot instance exactly as you would for a normal Chat SDK app, but keep the bot definition separate from the workflow code:
|
|
62
|
-
|
|
63
|
-
```typescript title="lib/bot.ts" lineNumbers
|
|
64
|
-
import { createRedisState } from "@chat-adapter/state-redis";
|
|
65
|
-
import { createSlackAdapter } from "@chat-adapter/slack";
|
|
66
|
-
import { Chat } from "chat";
|
|
67
|
-
|
|
68
|
-
const adapters = {
|
|
69
|
-
slack: createSlackAdapter(),
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
export interface ThreadState {
|
|
73
|
-
runId?: string;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export const bot = new Chat<typeof adapters, ThreadState>({
|
|
77
|
-
userName: "durable-bot",
|
|
78
|
-
adapters,
|
|
79
|
-
state: createRedisState(),
|
|
80
|
-
}).registerSingleton();
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
`runId` will store the active workflow run for each subscribed thread.
|
|
84
|
-
|
|
85
|
-
`registerSingleton()` matters here because Workflow may deserialize `Thread` objects again inside `"use step"` functions, and Chat SDK needs a registered singleton to resolve the adapter and state layer for those thread instances.
|
|
86
|
-
|
|
87
|
-
## Define a hook payload type
|
|
88
|
-
|
|
89
|
-
Workflow hooks are how follow-up messages get injected back into a running session. Define the payload type once so both the workflow and the webhook side stay in sync:
|
|
90
|
-
|
|
91
|
-
```typescript title="workflows/chat-turn-hook.ts" lineNumbers
|
|
92
|
-
import type { SerializedMessage } from "chat";
|
|
93
|
-
|
|
94
|
-
export type ChatTurnPayload = {
|
|
95
|
-
message: SerializedMessage;
|
|
96
|
-
};
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
## Create the durable session workflow
|
|
100
|
-
|
|
101
|
-
The workflow receives the serialized thread and first message, restores them with `bot.reviver()`, and then keeps waiting for more turns through the hook.
|
|
102
|
-
|
|
103
|
-
The important detail is that the workflow only orchestrates. Chat SDK side effects such as `post()`, `unsubscribe()`, and `setState()` stay inside step helpers:
|
|
104
|
-
|
|
105
|
-
```typescript title="workflows/durable-chat-session.ts" lineNumbers
|
|
106
|
-
import { Message, type Thread } from "chat";
|
|
107
|
-
import { createHook, getWorkflowMetadata } from "workflow";
|
|
108
|
-
import { bot, type ThreadState } from "@/lib/bot";
|
|
109
|
-
import type { ChatTurnPayload } from "@/workflows/chat-turn-hook";
|
|
110
|
-
|
|
111
|
-
async function postAssistantMessage(
|
|
112
|
-
thread: Thread<ThreadState>,
|
|
113
|
-
text: string
|
|
114
|
-
) {
|
|
115
|
-
"use step";
|
|
116
|
-
|
|
117
|
-
await bot.initialize();
|
|
118
|
-
await thread.post(text);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async function closeSession(thread: Thread<ThreadState>) {
|
|
122
|
-
"use step";
|
|
123
|
-
|
|
124
|
-
await bot.initialize();
|
|
125
|
-
await thread.post("Session closed.");
|
|
126
|
-
await thread.unsubscribe();
|
|
127
|
-
await thread.setState({}, { replace: true });
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async function runTurn(text: string) {
|
|
131
|
-
"use step";
|
|
132
|
-
|
|
133
|
-
// Replace this with AI SDK calls, database work, or other business logic.
|
|
134
|
-
return `You said: ${text}`;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
async function processMessage(
|
|
138
|
-
thread: Thread<ThreadState>,
|
|
139
|
-
message: Message
|
|
140
|
-
) {
|
|
141
|
-
const text = message.text.trim();
|
|
142
|
-
|
|
143
|
-
if (text.toLowerCase() === "done") {
|
|
144
|
-
await closeSession(thread);
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const reply = await runTurn(text);
|
|
149
|
-
await postAssistantMessage(thread, reply);
|
|
150
|
-
return true;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export async function durableChatSession(payload: string) {
|
|
154
|
-
"use workflow";
|
|
155
|
-
|
|
156
|
-
const { workflowRunId } = getWorkflowMetadata();
|
|
157
|
-
const { thread, message } = JSON.parse(payload, bot.reviver()) as {
|
|
158
|
-
thread: Thread<ThreadState>;
|
|
159
|
-
message: Message;
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
using hook = createHook<ChatTurnPayload>({ token: workflowRunId });
|
|
163
|
-
|
|
164
|
-
await postAssistantMessage(
|
|
165
|
-
thread,
|
|
166
|
-
"Durable session started. Reply in this thread and send `done` when you want to stop."
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
const shouldContinue = await processMessage(thread, message);
|
|
170
|
-
if (!shouldContinue) {
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
for await (const event of hook) {
|
|
175
|
-
const nextMessage = Message.fromJSON(event.message);
|
|
176
|
-
|
|
177
|
-
const keepRunning = await processMessage(thread, nextMessage);
|
|
178
|
-
if (!keepRunning) {
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
<Callout type="info">
|
|
186
|
-
The `using` keyword requires TypeScript 5.2+ with `"lib": ["esnext.disposable"]` in your `tsconfig.json`. If you are on an older version, call `hook.dispose()` manually when the session ends.
|
|
187
|
-
</Callout>
|
|
188
|
-
|
|
189
|
-
This is the core integration:
|
|
190
|
-
|
|
191
|
-
- `thread.toJSON()` and `message.toJSON()` cross the workflow boundary safely
|
|
192
|
-
- `bot.reviver()` restores real Chat SDK objects inside the workflow
|
|
193
|
-
- `bot.registerSingleton()` lets Workflow deserialize `Thread` objects again inside step functions
|
|
194
|
-
- `createHook<ChatTurnPayload>({ token: workflowRunId })` makes the workflow run itself the session identifier
|
|
195
|
-
- `runTurn()`, `postAssistantMessage()`, and `closeSession()` are steps, so adapter and state side effects stay outside the workflow sandbox
|
|
196
|
-
|
|
197
|
-
## Register Chat SDK event handlers
|
|
198
|
-
|
|
199
|
-
Create a small side-effect module that decides whether to start a new workflow or resume the existing one:
|
|
200
|
-
|
|
201
|
-
```typescript title="lib/chat-session-handlers.ts" lineNumbers
|
|
202
|
-
import { type Message, type Thread } from "chat";
|
|
203
|
-
import { resumeHook, start } from "workflow/api";
|
|
204
|
-
import { bot, type ThreadState } from "@/lib/bot";
|
|
205
|
-
import { durableChatSession } from "@/workflows/durable-chat-session";
|
|
206
|
-
import type { ChatTurnPayload } from "@/workflows/chat-turn-hook";
|
|
207
|
-
|
|
208
|
-
async function startSession(
|
|
209
|
-
thread: Thread<ThreadState>,
|
|
210
|
-
message: Message
|
|
211
|
-
) {
|
|
212
|
-
const run = await start(durableChatSession, [
|
|
213
|
-
JSON.stringify({
|
|
214
|
-
thread: thread.toJSON(),
|
|
215
|
-
message: message.toJSON(),
|
|
216
|
-
}),
|
|
217
|
-
]);
|
|
218
|
-
|
|
219
|
-
await thread.setState({ runId: run.runId });
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async function routeTurn(
|
|
223
|
-
thread: Thread<ThreadState>,
|
|
224
|
-
message: Message
|
|
225
|
-
) {
|
|
226
|
-
const state = await thread.state;
|
|
227
|
-
|
|
228
|
-
if (!state?.runId) {
|
|
229
|
-
await startSession(thread, message);
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
await resumeHook<ChatTurnPayload>(state.runId, {
|
|
234
|
-
message: message.toJSON(),
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
bot.onNewMention(async (thread, message) => {
|
|
239
|
-
await thread.subscribe();
|
|
240
|
-
await routeTurn(thread, message);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
bot.onSubscribedMessage(async (thread, message) => {
|
|
244
|
-
await routeTurn(thread, message);
|
|
245
|
-
});
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
On the first mention, the handler subscribes the thread and starts a workflow. Every later message resumes the existing run by sending the serialized message to the hook.
|
|
249
|
-
|
|
250
|
-
<Callout type="info">
|
|
251
|
-
In production, catch `resumeHook()` failures, clear stale `runId` values, and start a new session if the old workflow has already ended.
|
|
252
|
-
</Callout>
|
|
253
|
-
|
|
254
|
-
## Create the webhook route
|
|
255
|
-
|
|
256
|
-
Import the side-effect module once so the handlers are registered before the webhook runs:
|
|
257
|
-
|
|
258
|
-
```typescript title="app/api/webhooks/[platform]/route.ts" lineNumbers
|
|
259
|
-
import "@/lib/chat-session-handlers";
|
|
260
|
-
import { after } from "next/server";
|
|
261
|
-
import { bot } from "@/lib/bot";
|
|
262
|
-
|
|
263
|
-
type Platform = keyof typeof bot.webhooks;
|
|
264
|
-
|
|
265
|
-
export async function POST(
|
|
266
|
-
request: Request,
|
|
267
|
-
context: RouteContext<"/api/webhooks/[platform]">
|
|
268
|
-
) {
|
|
269
|
-
const { platform } = await context.params;
|
|
270
|
-
|
|
271
|
-
const handler = bot.webhooks[platform as Platform];
|
|
272
|
-
if (!handler) {
|
|
273
|
-
return new Response(`Unknown platform: ${platform}`, { status: 404 });
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return handler(request, {
|
|
277
|
-
waitUntil: (task) => after(() => task),
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
## Replace the step with AI
|
|
283
|
-
|
|
284
|
-
The workflow pattern stays the same if you want AI responses. Replace `runTurn()` with a step that calls AI SDK:
|
|
285
|
-
|
|
286
|
-
```typescript title="workflows/durable-chat-session.ts" lineNumbers
|
|
287
|
-
import { anthropic } from "@ai-sdk/anthropic";
|
|
288
|
-
import { generateText } from "ai";
|
|
289
|
-
|
|
290
|
-
async function runTurn(text: string) {
|
|
291
|
-
"use step";
|
|
292
|
-
|
|
293
|
-
const { text: reply } = await generateText({
|
|
294
|
-
model: anthropic("claude-sonnet-4-5"),
|
|
295
|
-
system: "You are a helpful assistant in a chat thread.",
|
|
296
|
-
prompt: text,
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
return reply;
|
|
300
|
-
}
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
Install the extra packages if you use this version:
|
|
304
|
-
|
|
305
|
-
```sh title="Terminal"
|
|
306
|
-
pnpm add ai @ai-sdk/anthropic
|
|
307
|
-
```
|
|
308
|
-
|
|
309
|
-
## How the pattern works
|
|
310
|
-
|
|
311
|
-
1. A user @mentions the bot in a thread.
|
|
312
|
-
2. Chat SDK subscribes the thread and starts `durableChatSession()`.
|
|
313
|
-
3. The handler stores the workflow `runId` in Chat SDK thread state.
|
|
314
|
-
4. Follow-up messages call `resumeHook(runId, ...)` instead of starting a new run.
|
|
315
|
-
5. The workflow keeps ownership of the session until the user sends `done` or you end it some other way.
|
|
316
|
-
|
|
317
|
-
This gives you a durable session boundary without moving platform-specific webhook code into your workflow layer.
|
|
318
|
-
|
|
319
|
-
From here you can add:
|
|
320
|
-
|
|
321
|
-
- inactivity timeouts with Workflow `sleep()`
|
|
322
|
-
- escalation or approval pauses with additional hooks
|
|
323
|
-
- AI-generated replies, tool calls, or human handoffs inside `"use step"` functions
|
|
324
|
-
|
|
325
|
-
## Next steps
|
|
326
|
-
|
|
327
|
-
- [Slack bot with Next.js and Redis](/docs/guides/slack-nextjs) — Slack app setup and basic webhook wiring
|
|
328
|
-
- [Handling Events](/docs/handling-events) — Mentions, subscribed messages, and routing behavior
|
|
329
|
-
- [Streaming](/docs/streaming) — Stream AI SDK responses directly to chat platforms
|
|
330
|
-
- [Thread API](/docs/api/thread) — `thread.toJSON()`, `thread.setState()`, and other thread primitives
|
|
331
|
-
- [Chat API](/docs/api/chat) — `bot.reviver()`, initialization, and webhook access
|