chat 4.28.1 → 4.30.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.
Files changed (48) hide show
  1. package/dist/ai/index.d.ts +501 -0
  2. package/dist/ai/index.js +500 -0
  3. package/dist/chat-BPjXsoIl.d.ts +3158 -0
  4. package/dist/chunk-HD375J7S.js +128 -0
  5. package/dist/index.d.ts +6 -3143
  6. package/dist/index.js +45 -139
  7. package/dist/{jsx-runtime-DxGwoLu2.d.ts → jsx-runtime-CnDs8rPr.d.ts} +1 -1
  8. package/dist/jsx-runtime.d.ts +1 -1
  9. package/docs/adapters.mdx +35 -5
  10. package/docs/ai/ai-sdk-tools.mdx +227 -0
  11. package/docs/ai/index.mdx +69 -0
  12. package/docs/ai/meta.json +4 -0
  13. package/docs/{api → ai}/to-ai-messages.mdx +16 -3
  14. package/docs/ai/types.mdx +243 -0
  15. package/docs/api/chat.mdx +50 -10
  16. package/docs/api/index.mdx +4 -6
  17. package/docs/api/message.mdx +1 -1
  18. package/docs/api/meta.json +0 -1
  19. package/docs/api/postable-message.mdx +3 -3
  20. package/docs/api/thread.mdx +1 -1
  21. package/docs/concurrency.mdx +54 -15
  22. package/docs/contributing/building.mdx +1 -1
  23. package/docs/contributing/testing.mdx +4 -0
  24. package/docs/direct-messages.mdx +10 -1
  25. package/docs/files.mdx +20 -0
  26. package/docs/getting-started.mdx +5 -1
  27. package/docs/handling-events.mdx +10 -7
  28. package/docs/index.mdx +4 -1
  29. package/docs/meta.json +4 -0
  30. package/docs/posting-messages.mdx +3 -1
  31. package/docs/slack-primitives.mdx +320 -0
  32. package/docs/slash-commands.mdx +4 -4
  33. package/docs/streaming.mdx +4 -4
  34. package/docs/subject.mdx +1 -1
  35. package/docs/testing.mdx +142 -0
  36. package/docs/threads-messages-channels.mdx +1 -1
  37. package/docs/usage.mdx +8 -4
  38. package/package.json +23 -2
  39. package/resources/guides/create-a-discord-support-bot-with-nuxt-and-redis.md +5 -1
  40. package/resources/guides/how-to-build-a-slack-bot-with-next-js-and-redis.md +5 -1
  41. package/resources/guides/how-to-build-an-ai-agent-for-slack-with-chat-sdk-and-ai-sdk.md +1 -1
  42. package/resources/guides/human-in-the-loop-with-chat-sdk-and-workflow-sdk.md +176 -0
  43. package/resources/guides/liveblocks-chat-sdk-ai-sdk.md +165 -0
  44. package/resources/guides/run-and-track-deploys-from-slack.md +7 -5
  45. package/resources/guides/ship-a-github-code-review-bot-with-hono-and-redis.md +5 -1
  46. package/resources/guides/slack-bot-vercel-blob.md +254 -0
  47. package/resources/guides/triage-form-submissions-with-chat-sdk.md +3 -1
  48. package/resources/templates.json +5 -0
@@ -2,18 +2,31 @@
2
2
  title: toAiMessages
3
3
  description: Convert Chat SDK messages to AI SDK conversation format.
4
4
  type: reference
5
+ related:
6
+ - /docs/ai
7
+ - /docs/ai/ai-sdk-tools
8
+ - /docs/ai/types
9
+ - /docs/streaming
5
10
  ---
6
11
 
7
- Convert an array of `Message` objects into the `{ role, content }[]` format expected by AI SDKs. The output is structurally compatible with AI SDK's `ModelMessage[]`.
12
+ Convert an array of [`Message`](/docs/api/message) objects into the `{ role, content }[]` format expected by the AI SDK. The output is structurally compatible with AI SDK's `ModelMessage[]`.
8
13
 
9
14
  ```typescript
10
- import { toAiMessages } from "chat";
15
+ import { toAiMessages } from "chat/ai";
11
16
  ```
12
17
 
18
+ <Callout>
19
+ `toAiMessages` is also re-exported from the main `chat` entrypoint
20
+ for backwards compatibility (with a `@deprecated` JSDoc hint), but
21
+ new code should import it from [`chat/ai`](/docs/ai) alongside
22
+ [`createChatTools`](/docs/ai/ai-sdk-tools) and the rest of the AI
23
+ utilities.
24
+ </Callout>
25
+
13
26
  ## Usage
14
27
 
15
28
  ```typescript title="lib/bot.ts" lineNumbers
16
- import { toAiMessages } from "chat";
29
+ import { toAiMessages } from "chat/ai";
17
30
 
18
31
  bot.onSubscribedMessage(async (thread, message) => {
19
32
  const result = await thread.adapter.fetchMessages(thread.id, { limit: 20 });
@@ -0,0 +1,243 @@
1
+ ---
2
+ title: Types
3
+ description: TypeScript types exported from the chat/ai subpath.
4
+ type: reference
5
+ related:
6
+ - /docs/ai
7
+ - /docs/ai/ai-sdk-tools
8
+ - /docs/ai/to-ai-messages
9
+ ---
10
+
11
+ Every type exported from `chat/ai`. Pulling these from the subpath keeps the optional `ai` and `zod` peer deps out of bundles that don't import them.
12
+
13
+ ```ts
14
+ import type {
15
+ AiMessage,
16
+ AiUserMessage,
17
+ AiAssistantMessage,
18
+ AiMessagePart,
19
+ AiTextPart,
20
+ AiImagePart,
21
+ AiFilePart,
22
+ ToAiMessagesOptions,
23
+ ChatBinding,
24
+ ChatTools,
25
+ ChatToolName,
26
+ ChatToolPreset,
27
+ ChatWriteToolName,
28
+ ApprovalConfig,
29
+ ToolOptions,
30
+ ToolOverrides,
31
+ } from "chat/ai";
32
+ ```
33
+
34
+ ## Conversation messages
35
+
36
+ Used by [`toAiMessages`](/docs/ai/to-ai-messages) and any agent prompt you build by hand. The shapes are structurally compatible with AI SDK's `ModelMessage` so the result is directly assignable to `prompt` / `messages`.
37
+
38
+ ### AiMessage
39
+
40
+ ```typescript
41
+ type AiMessage = AiUserMessage | AiAssistantMessage;
42
+ ```
43
+
44
+ A single normalized turn in a conversation — the array form is what AI SDK calls expect.
45
+
46
+ ### AiUserMessage
47
+
48
+ ```typescript
49
+ interface AiUserMessage {
50
+ role: "user";
51
+ content: string | AiMessagePart[];
52
+ }
53
+ ```
54
+
55
+ User content can be plain text, or a multipart array when attachments are present.
56
+
57
+ ### AiAssistantMessage
58
+
59
+ ```typescript
60
+ interface AiAssistantMessage {
61
+ role: "assistant";
62
+ content: string;
63
+ }
64
+ ```
65
+
66
+ Assistant turns are always plain strings — `toAiMessages` produces this for any message authored by the bot itself (`author.isMe === true`).
67
+
68
+ ### AiMessagePart
69
+
70
+ ```typescript
71
+ type AiMessagePart = AiTextPart | AiImagePart | AiFilePart;
72
+ ```
73
+
74
+ The discriminated union used inside multipart user messages.
75
+
76
+ ### AiTextPart
77
+
78
+ ```typescript
79
+ interface AiTextPart {
80
+ type: "text";
81
+ text: string;
82
+ }
83
+ ```
84
+
85
+ ### AiImagePart
86
+
87
+ ```typescript
88
+ interface AiImagePart {
89
+ type: "image";
90
+ image: DataContent | URL;
91
+ mediaType?: string;
92
+ }
93
+ ```
94
+
95
+ `DataContent` matches AI SDK's type — `string | Uint8Array | ArrayBuffer | Buffer`.
96
+
97
+ ### AiFilePart
98
+
99
+ ```typescript
100
+ interface AiFilePart {
101
+ type: "file";
102
+ data: DataContent | URL;
103
+ filename?: string;
104
+ mediaType: string;
105
+ }
106
+ ```
107
+
108
+ `toAiMessages` emits text-like attachments (JSON, XML, YAML, source files, etc.) as file parts.
109
+
110
+ ### ToAiMessagesOptions
111
+
112
+ ```typescript
113
+ interface ToAiMessagesOptions {
114
+ includeNames?: boolean;
115
+ transformMessage?: (
116
+ aiMessage: AiMessage,
117
+ source: Message
118
+ ) => AiMessage | null | Promise<AiMessage | null>;
119
+ onUnsupportedAttachment?: (
120
+ attachment: Attachment,
121
+ message: Message
122
+ ) => void;
123
+ }
124
+ ```
125
+
126
+ See [`toAiMessages`](/docs/ai/to-ai-messages) for behavior and examples.
127
+
128
+ ## Tools
129
+
130
+ Returned by [`createChatTools`](/docs/ai/ai-sdk-tools) and used to configure it.
131
+
132
+ ### ChatBinding
133
+
134
+ ```typescript
135
+ type ChatBinding = Chat<any, any>;
136
+ ```
137
+
138
+ Whatever [`Chat`](/docs/api/chat) instance the tools should dispatch operations against. The generics are intentionally loose so any strongly-typed `Chat<TAdapters, TState>` is assignable.
139
+
140
+ ### ChatTools
141
+
142
+ ```typescript
143
+ type ChatTools = ReturnType<typeof createChatTools>;
144
+ ```
145
+
146
+ Convenience alias for the object returned by `createChatTools` — handy when you want to type a wrapper or pass the toolset around.
147
+
148
+ ### ChatToolPreset
149
+
150
+ ```typescript
151
+ type ChatToolPreset = "reader" | "messenger" | "moderator";
152
+ ```
153
+
154
+ Predefined toolset scopes. See [Presets](/docs/ai/ai-sdk-tools#presets) for the exact tool list per preset.
155
+
156
+ ### ChatToolName
157
+
158
+ ```typescript
159
+ type ChatToolName =
160
+ | "fetchMessages"
161
+ | "fetchChannelMessages"
162
+ | "fetchThread"
163
+ | "listThreads"
164
+ | "getThreadParticipants"
165
+ | "getChannelInfo"
166
+ | "getUser"
167
+ | "startTyping"
168
+ | "postMessage"
169
+ | "postChannelMessage"
170
+ | "sendDirectMessage"
171
+ | "editMessage"
172
+ | "deleteMessage"
173
+ | "addReaction"
174
+ | "removeReaction"
175
+ | "subscribeThread"
176
+ | "unsubscribeThread";
177
+ ```
178
+
179
+ The names of every generated tool. Useful when typing per-tool overrides.
180
+
181
+ ### ChatWriteToolName
182
+
183
+ ```typescript
184
+ type ChatWriteToolName =
185
+ | "postMessage"
186
+ | "postChannelMessage"
187
+ | "sendDirectMessage"
188
+ | "editMessage"
189
+ | "deleteMessage"
190
+ | "addReaction"
191
+ | "removeReaction"
192
+ | "subscribeThread"
193
+ | "unsubscribeThread";
194
+ ```
195
+
196
+ The names of every mutating tool. Useful when wiring per-tool approval overrides.
197
+
198
+ ### ApprovalConfig
199
+
200
+ ```typescript
201
+ type ApprovalConfig =
202
+ | boolean
203
+ | Partial<Record<ChatWriteToolName, boolean>>;
204
+ ```
205
+
206
+ Controls the `requireApproval` option:
207
+
208
+ - `true` (default) — every write tool needs approval.
209
+ - `false` — no write tool needs approval.
210
+ - object — per-tool override; unspecified write tools fall back to `true`.
211
+
212
+ ### ToolOptions
213
+
214
+ ```typescript
215
+ interface ToolOptions {
216
+ needsApproval?: boolean;
217
+ }
218
+ ```
219
+
220
+ Common options accepted by every standalone write-tool factory (e.g. `postMessage(chat, { needsApproval: false })`).
221
+
222
+ ### ToolOverrides
223
+
224
+ ```typescript
225
+ type ToolOverrides = Partial<
226
+ Pick<
227
+ Tool,
228
+ | "description"
229
+ | "inputExamples"
230
+ | "metadata"
231
+ | "needsApproval"
232
+ | "onInputAvailable"
233
+ | "onInputDelta"
234
+ | "onInputStart"
235
+ | "providerOptions"
236
+ | "strict"
237
+ | "title"
238
+ | "toModelOutput"
239
+ >
240
+ >;
241
+ ```
242
+
243
+ Per-tool overrides accepted by `createChatTools({ overrides })`. Core fields like `execute`, `inputSchema`, `outputSchema`, `type`, `id`, and `args` are intentionally excluded so tool semantics stay stable across upgrades.
package/docs/api/chat.mdx CHANGED
@@ -74,9 +74,36 @@ bot.onNewMention(async (thread, message) => {
74
74
  }}
75
75
  />
76
76
 
77
+ ### onDirectMessage
78
+
79
+ Fires for every direct message when registered. Direct message handlers run before `onSubscribedMessage`, `onNewMention`, and pattern handlers. If no direct message handler is registered, unsubscribed DMs fall through to `onNewMention` for backward compatibility.
80
+
81
+ ```typescript
82
+ bot.onDirectMessage(async (thread, message, channel) => {
83
+ await thread.post(`Got your DM in ${channel.id}: ${message.text}`);
84
+ });
85
+ ```
86
+
87
+ <TypeTable
88
+ type={{
89
+ thread: {
90
+ description: 'The DM thread where the message occurred.',
91
+ type: 'Thread',
92
+ },
93
+ message: {
94
+ description: 'The direct message.',
95
+ type: 'Message',
96
+ },
97
+ channel: {
98
+ description: 'The DM channel.',
99
+ type: 'Channel',
100
+ },
101
+ }}
102
+ />
103
+
77
104
  ### onSubscribedMessage
78
105
 
79
- Fires for every new message in a subscribed thread. Once subscribed, all messages (including @-mentions) route here instead of `onNewMention`.
106
+ Fires for every new message in a subscribed non-DM thread. Once subscribed, messages (including @-mentions) route here instead of `onNewMention`. DM threads route to `onDirectMessage` first when a direct message handler is registered.
80
107
 
81
108
  ```typescript
82
109
  bot.onSubscribedMessage(async (thread, message) => {
@@ -474,16 +501,20 @@ const slack = bot.getAdapter("slack");
474
501
 
475
502
  #### Direct client access
476
503
 
477
- Use `.client` to access the platform's typed native API client directly — available on Linear and GitHub:
504
+ Access the platform's typed native API client directly via an SDK-named getter `.webClient` on Slack, `.linearClient` on Linear, `.octokit` on GitHub:
478
505
 
479
506
  ```typescript
507
+ // Slack - full WebClient from @slack/web-api
508
+ const slack = bot.getAdapter("slack").webClient;
509
+ await slack.pins.add({ channel: "C123ABC", timestamp: "1234567890.123456" });
510
+
480
511
  // Linear - full LinearClient from @linear/sdk
481
- const linear = bot.getAdapter("linear").client;
512
+ const linear = bot.getAdapter("linear").linearClient;
482
513
  const issue = await linear.issue("ENG-123");
483
514
  const project = await issue.project;
484
515
 
485
516
  // GitHub - full Octokit from @octokit/rest
486
- const github = bot.getAdapter("github").client;
517
+ const github = bot.getAdapter("github").octokit;
487
518
  const { data: pulls } = await github.rest.pulls.list({
488
519
  owner: "vercel",
489
520
  repo: "chat",
@@ -491,16 +522,25 @@ const { data: pulls } = await github.rest.pulls.list({
491
522
  });
492
523
  ```
493
524
 
494
- The client uses the credentials from your adapter config. For multi-tenant adapters (Linear, GitHub), it returns the client for the current webhook request context.
525
+ The client uses the credentials from your adapter config. For multi-tenant / multi-workspace adapters (Slack, Linear, GitHub), it returns the client bound to the credentials for the current webhook request context.
526
+
527
+ <Callout type="info">
528
+ The previous `.client` getter still works on all three adapters as a deprecated alias for `.webClient` / `.linearClient` / `.octokit`.
529
+ </Callout>
495
530
 
496
531
  <Callout type="warn">
497
- For multi-tenant adapters (GitHub App without a fixed installation ID, Linear with per-org OAuth), `client` requires webhook handler context to resolve credentials. Calling it outside a handler throws. Single-tenant adapters (PAT, API key) work anywhere.
532
+ Multi-tenant adapters (GitHub App without a fixed installation ID, Linear with per-org OAuth, Slack in multi-workspace mode) require a webhook handler context to resolve credentials when the native client getter is accessed. Calling it outside a handler throws.
533
+
534
+ For Slack, you can also bind a token explicitly outside a webhook with `adapter.withBotToken(token, () => adapter.webClient.…)` — useful for cron jobs or workflows. The same pattern is required when `botToken` is configured as an async resolver function, since `.webClient` resolves the token synchronously.
535
+
536
+ Single-tenant adapters (PAT, API key, static `botToken` string, or a synchronous `botToken` resolver) work anywhere.
498
537
  </Callout>
499
538
 
500
- | Adapter | `client` type |
501
- |---------|---------------|
502
- | Linear | `LinearClient` from `@linear/sdk` |
503
- | GitHub | `Octokit` from `@octokit/rest` |
539
+ | Adapter | Getter | Type |
540
+ |---------|--------|------|
541
+ | Slack | `.webClient` | `WebClient` from `@slack/web-api` |
542
+ | Linear | `.linearClient` | `LinearClient` from `@linear/sdk` |
543
+ | GitHub | `.octokit` | `Octokit` from `@octokit/rest` |
504
544
 
505
545
  ### openDM
506
546
 
@@ -20,12 +20,6 @@ import { Chat, root, paragraph, text, Card, Button, emoji } from "chat";
20
20
  | [`Message`](/docs/api/message) | Normalized message with text, AST, author, and metadata |
21
21
  | [`ScheduledMessage`](/docs/api/thread#scheduledmessage) | Returned by `thread.schedule()` / `channel.schedule()` with `cancel()` |
22
22
 
23
- ## Utilities
24
-
25
- | Export | Description |
26
- |--------|-------------|
27
- | [`toAiMessages`](/docs/api/to-ai-messages) | Convert `Message[]` to AI SDK `{ role, content }[]` format |
28
-
29
23
  ## Message formats
30
24
 
31
25
  | Export | Description |
@@ -36,3 +30,7 @@ import { Chat, root, paragraph, text, Card, Button, emoji } from "chat";
36
30
  | [`Cards`](/docs/api/cards) | Rich card components — `Card`, `Text`, `Button`, `Actions`, etc. |
37
31
  | [`Markdown`](/docs/api/markdown) | AST builder functions — `root`, `paragraph`, `text`, `strong`, etc. |
38
32
  | [`Modals`](/docs/api/modals) | Modal form components — `Modal`, `TextInput`, `Select`, etc. |
33
+
34
+ ## AI utilities
35
+
36
+ `toAiMessages`, `createChatTools`, and the supporting types live in the [`chat/ai`](/docs/ai) subpath — see the [AI section](/docs/ai) for the full reference.
@@ -194,7 +194,7 @@ Links found in incoming messages are extracted and exposed as `LinkPreview` obje
194
194
  />
195
195
 
196
196
  <Callout type="info">
197
- When using [`toAiMessages()`](/docs/api/to-ai-messages), link metadata is automatically appended to the message content. Embedded message links are labeled as `[Embedded message: ...]` so the AI model understands the context.
197
+ When using [`toAiMessages()`](/docs/ai/to-ai-messages), link metadata is automatically appended to the message content. Embedded message links are labeled as `[Embedded message: ...]` so the AI model understands the context.
198
198
  </Callout>
199
199
 
200
200
  ### Platform support
@@ -6,7 +6,6 @@
6
6
  "thread",
7
7
  "channel",
8
8
  "message",
9
- "to-ai-messages",
10
9
  "postable-message",
11
10
  "transcripts",
12
11
  "cards",
@@ -38,7 +38,7 @@ await thread.post({ raw: "Hello world" });
38
38
  type: 'string',
39
39
  },
40
40
  attachments: {
41
- description: 'File/image attachments.',
41
+ description: 'Typed media attachments for adapters that support outgoing attachments.',
42
42
  type: 'Attachment[]',
43
43
  },
44
44
  files: {
@@ -63,7 +63,7 @@ await thread.post({ markdown: "**Bold** and _italic_" });
63
63
  type: 'string',
64
64
  },
65
65
  attachments: {
66
- description: 'File/image attachments.',
66
+ description: 'Typed media attachments for adapters that support outgoing attachments.',
67
67
  type: 'Attachment[]',
68
68
  },
69
69
  files: {
@@ -92,7 +92,7 @@ await thread.post({
92
92
  type: 'Root',
93
93
  },
94
94
  attachments: {
95
- description: 'File/image attachments.',
95
+ description: 'Typed media attachments for adapters that support outgoing attachments.',
96
96
  type: 'Attachment[]',
97
97
  },
98
98
  files: {
@@ -146,7 +146,7 @@ if (participants.length > 1) {
146
146
 
147
147
  ## subscribe / unsubscribe
148
148
 
149
- Manage thread subscriptions. Subscribed threads route all messages to `onSubscribedMessage` handlers.
149
+ Manage thread subscriptions. Subscribed non-DM threads route all messages to `onSubscribedMessage` handlers. DM threads route to `onDirectMessage` first when a direct message handler is registered.
150
150
 
151
151
  ```typescript
152
152
  await thread.subscribe();
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Overlapping Messages
3
- description: Control how overlapping messages on the same thread are handled queue, debounce, drop, or process concurrently.
3
+ description: Control how overlapping messages on the same thread are handled - burst, queue, debounce, drop, or process concurrently.
4
4
  type: guide
5
5
  prerequisites:
6
6
  - /docs/handling-events
@@ -57,11 +57,49 @@ A done → drain: [B, C, D] → handler(D, { skipped: [B, C] })
57
57
  D done → queue empty → release lock
58
58
  ```
59
59
 
60
+ ### Burst
61
+
62
+ Waits for `debounceMs` before the first handler on an idle thread, then drains the collected burst like `queue`. The latest message is dispatched, and earlier messages in the burst are available as `context.skipped`.
63
+
64
+ Use this for assistant-style bots where users often send one logical turn as several short messages inside a small window and you want one response with full context. Compared to `debounce`, `burst` keeps the earlier messages in `context.skipped` instead of dropping them, and it flushes queued messages that arrive while the handler is running.
65
+
66
+ Choose `debounce` when the latest message replaces earlier ones, like rapid corrections. Choose `burst` when earlier messages still matter, like "hey", "quick question", and then the actual question. The tradeoff is that even a lone message waits for `debounceMs` before the handler runs.
67
+
68
+ ```typescript title="lib/bot.ts" lineNumbers
69
+ const bot = new Chat({
70
+ concurrency: { strategy: "burst", debounceMs: 1000 },
71
+ // ...
72
+ });
73
+
74
+ bot.onNewMention(async (thread, message, context) => {
75
+ const turn = [...(context?.skipped ?? []), message]
76
+ .map((m) => m.text)
77
+ .join("\n\n");
78
+
79
+ const response = await generateAIResponse(turn);
80
+ await thread.post(response);
81
+ });
82
+ ```
83
+
84
+ **Flow:**
85
+
86
+ ```
87
+ A arrives → acquire lock → enqueue A → sleep(debounceMs)
88
+ B arrives → lock busy → enqueue B
89
+ C arrives → lock busy → enqueue C
90
+ ... debounceMs elapses ...
91
+ → drain: [A, B, C] → handler(C, { skipped: [A, B] })
92
+ D arrives while C is running → lock busy → enqueue D
93
+ E arrives while C is running → lock busy → enqueue E
94
+ C done → drain: [D, E] → handler(E, { skipped: [D] })
95
+ E done → queue empty → release lock
96
+ ```
97
+
60
98
  ### Debounce
61
99
 
62
- Every message starts or resets a debounce timer. Only the **final message in a burst** is processed.
100
+ The first message waits for `debounceMs`. Messages that arrive during that window replace the pending message, so only the **final message in the burst window** is processed.
63
101
 
64
- This is particularly useful for platforms like **WhatsApp** and **Telegram** where users tend to send a flurry of short messages in quick succession instead of composing a single message "hey", "quick question", "how do I reset my password?" arriving as three separate webhooks within a few seconds. Without debounce, the bot would respond to "hey" before the actual question even arrives. With debounce, the SDK waits for a pause in the conversation and processes only the final message.
102
+ This is particularly useful for platforms like **WhatsApp** and **Telegram** where users tend to send a flurry of short messages in quick succession instead of composing a single message - "hey", "quick question", "how do I reset my password?" arriving as three separate webhooks within a few seconds. Without debounce, the bot would respond to "hey" before the actual question even arrives. With debounce, the SDK waits briefly and processes only the final message in the window.
65
103
 
66
104
  ```typescript title="lib/bot.ts" lineNumbers
67
105
  const bot = new Chat({
@@ -118,10 +156,10 @@ const bot = new Chat({
118
156
  | Option | Strategies | Default | Description |
119
157
  |--------|-----------|---------|-------------|
120
158
  | `strategy` | all | `"drop"` | The concurrency strategy to use |
121
- | `maxQueueSize` | queue, debounce | `10` | Maximum queued messages per thread |
122
- | `onQueueFull` | queue, debounce | `"drop-oldest"` | Whether to evict the oldest or reject the newest message when the queue is full |
123
- | `queueEntryTtlMs` | queue, debounce | `90000` | TTL for queued entries in milliseconds. Expired entries are discarded on dequeue |
124
- | `debounceMs` | debounce | `1500` | Debounce window in milliseconds |
159
+ | `maxQueueSize` | queue, burst | `10` | Maximum queued messages per thread |
160
+ | `onQueueFull` | queue, burst | `"drop-oldest"` | Whether to evict the oldest or reject the newest message when the queue is full |
161
+ | `queueEntryTtlMs` | queue, debounce, burst | `90000` | TTL for queued entries in milliseconds. Expired entries are discarded on dequeue |
162
+ | `debounceMs` | debounce, burst | `1500` | Debounce window in milliseconds |
125
163
  | `maxConcurrent` | concurrent | `Infinity` | Max concurrent handlers per thread |
126
164
 
127
165
  <Callout type="warn">
@@ -130,7 +168,7 @@ const bot = new Chat({
130
168
 
131
169
  ## MessageContext
132
170
 
133
- All handler types (`onNewMention`, `onSubscribedMessage`, `onNewMessage`) accept an optional `MessageContext` as their last parameter. It is only populated when using the `queue` strategy and messages were skipped.
171
+ All handler types (`onNewMention`, `onSubscribedMessage`, `onNewMessage`) accept an optional `MessageContext` as their last parameter. It is populated when using the `queue` strategy for queued messages, and when using `burst` for a collapsed turn. A lone `burst` message receives `skipped: []` and `totalSinceLastHandler: 1`.
134
172
 
135
173
  ```typescript
136
174
  interface MessageContext {
@@ -186,7 +224,7 @@ const bot = new Chat({
186
224
 
187
225
  ## State adapter requirements
188
226
 
189
- The `queue` and `debounce` strategies require three additional methods on your state adapter:
227
+ The `queue`, `debounce`, and `burst` strategies require three additional methods on your state adapter:
190
228
 
191
229
  | Method | Description |
192
230
  |--------|-------------|
@@ -202,12 +240,12 @@ All strategies emit structured log events at `info` level:
202
240
 
203
241
  | Event | Strategy | Data |
204
242
  |-------|----------|------|
205
- | `message-queued` | queue | threadId, messageId, queueDepth |
206
- | `message-dequeued` | queue, debounce | threadId, messageId, skippedCount |
207
- | `message-dropped` | drop, queue | threadId, messageId, reason |
208
- | `message-expired` | queue, debounce | threadId, messageId |
243
+ | `message-queued` | queue, burst | threadId, messageId, queueDepth |
244
+ | `message-dequeued` | queue, debounce, burst | threadId, messageId, skippedCount for queue/burst |
245
+ | `message-dropped` | drop, queue, burst | threadId, messageId, reason |
246
+ | `message-expired` | queue, debounce, burst | threadId, messageId |
209
247
  | `message-superseded` | debounce | threadId, droppedId |
210
- | `message-debouncing` | debounce | threadId, messageId, debounceMs |
248
+ | `message-debouncing` | debounce, burst | threadId, messageId, debounceMs |
211
249
  | `message-debounce-reset` | debounce | threadId, messageId |
212
250
 
213
251
  ## Choosing a strategy
@@ -216,7 +254,8 @@ All strategies emit structured log events at `info` level:
216
254
  |----------|----------|-----|
217
255
  | Simple bots, one-shot commands | `drop` | No complexity, no queue overhead |
218
256
  | AI chatbots, customer support | `queue` | Never lose messages; handler sees full conversation context |
219
- | WhatsApp/Telegram bots, rapid corrections | `debounce` | Users send many short messages in quick succession; wait for a pause before responding |
257
+ | AI chatbots with multi-message user turns | `burst` | Wait for the idle burst window, then respond once with every message in that window |
258
+ | WhatsApp/Telegram bots, rapid corrections | `debounce` | Users send many short messages in quick succession; wait briefly and keep only the latest |
220
259
  | Stateless lookups, translations | `concurrent` | Maximum throughput, no ordering needed |
221
260
 
222
261
  ## Backward compatibility
@@ -370,7 +370,7 @@ parseMessage(raw: unknown): Message<unknown> {
370
370
 
371
371
  ### Sending messages
372
372
 
373
- Use `extractCard()` and `extractFiles()` from `@chat-adapter/shared` to check for rich content. Use your format converter's `renderPostable()` to convert the message to platform format.
373
+ Use `extractCard()` and `extractFiles()` from `@chat-adapter/shared` to check for rich content. Use `extractPostableAttachments()` if your adapter maps normalized `Attachment` objects to platform-native media uploads. Use your format converter's `renderPostable()` to convert the message to platform format.
374
374
 
375
375
  ```typescript title="src/adapter.ts" lineNumbers
376
376
  async postMessage(
@@ -14,6 +14,10 @@ Chat SDK adapters are the trust boundary between your application and a platform
14
14
 
15
15
  All adapters in this repo use [vitest](https://vitest.dev) with `@vitest/coverage-v8`. Community adapters should follow the same convention.
16
16
 
17
+ <Callout type="info">
18
+ This page covers the hand-rolled patterns used inside this repo's `packages/`. If you're testing a bot or a custom adapter as a **consumer** of Chat SDK, use [`@chat-adapter/tests`](/docs/testing) — it ships factories and Vitest matchers that cover most of these patterns in a few lines.
19
+ </Callout>
20
+
17
21
  ## Unit tests
18
22
 
19
23
  ### Factory function
@@ -12,9 +12,18 @@ Open direct message conversations with users using `bot.openDM()`. For globally
12
12
 
13
13
  DMs behave slightly differently from channel messages:
14
14
 
15
- - **Implicit mentions** — DM messages automatically set `isMention=true`, so your `onNewMention` handler fires without the user needing to @-mention the bot. This feels natural for 1:1 conversations.
15
+ - **Direct message handlers** — if you register `onDirectMessage`, every incoming DM routes there before `onSubscribedMessage`, `onNewMention`, and pattern handlers. This keeps DM-centric flows like WhatsApp conversations, Telegram DMs, and web chat on one consistent handler.
16
+ - **Mention fallback** — if no `onDirectMessage` handlers are registered, DMs continue through normal routing. Unsubscribed DMs are treated as mentions, so existing `onNewMention` bots keep working without requiring the user to @-mention the bot.
16
17
  - **Per-conversation threading** — Each top-level DM starts a new conversation. Thread replies within a DM continue the same conversation, giving you the same per-thread isolation as channels.
17
18
 
19
+ ## Handle incoming DMs
20
+
21
+ ```typescript title="lib/bot.ts" lineNumbers
22
+ bot.onDirectMessage(async (thread, message) => {
23
+ await thread.post(`You said: ${message.text}`);
24
+ });
25
+ ```
26
+
18
27
  ## Open a DM
19
28
 
20
29
  ### From an Author object
package/docs/files.mdx CHANGED
@@ -25,6 +25,26 @@ await thread.post({
25
25
  });
26
26
  ```
27
27
 
28
+ ### Typed attachments
29
+
30
+ Use `attachments` when you already have normalized `Attachment` objects and the adapter supports typed outgoing media. Telegram supports one outgoing attachment per message and uses the native media method for the attachment type:
31
+
32
+ ```typescript title="lib/bot.ts" lineNumbers
33
+ await thread.post({
34
+ markdown: "Here's the image:",
35
+ attachments: [
36
+ {
37
+ data: imageBuffer,
38
+ name: "diagram.png",
39
+ mimeType: "image/png",
40
+ type: "image",
41
+ },
42
+ ],
43
+ });
44
+ ```
45
+
46
+ Outgoing `attachments` are available on `{ raw }`, `{ markdown }`, and `{ ast }` messages. Card messages use `files` for uploads. Use `files` for generic uploads. On Telegram, `files` always upload as documents, while `attachments` preserve image, audio, video, or file media type. Use `data` or `fetchData` for private/authenticated files; URL-only attachments must be public URLs Telegram can fetch directly.
47
+
28
48
  ### Multiple files
29
49
 
30
50
  ```typescript title="lib/bot.ts" lineNumbers
@@ -25,4 +25,8 @@ Connect your bot to chat platforms and persist state across restarts.
25
25
 
26
26
  Browse all official and community adapters on the [Adapters](/adapters) page.
27
27
 
28
- Step-by-step guides and starter templates are available on the [Resources](/resources) page.
28
+ ## Resources
29
+
30
+ - [The Complete Guide to Chat SDK](https://vercel.com/kb/guide/the-complete-guide-to-chat-sdk) — End-to-end walkthrough that takes you from zero to a deployed multi-platform bot, covering adapters, state, handlers, cards, and streaming.
31
+
32
+ See all guides and templates on the [resources](/resources) page.