chat 4.27.0 → 4.29.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/ai/index.d.ts +501 -0
- package/dist/ai/index.js +500 -0
- package/dist/chat-D9UYaaNO.d.ts +3156 -0
- package/dist/chunk-HD375J7S.js +128 -0
- package/dist/{chunk-AN7MRAVW.js → chunk-V25FKIIL.js} +5 -1
- package/dist/index.d.ts +35 -2934
- package/dist/index.js +567 -210
- package/dist/{jsx-runtime-Co9uV6l7.d.ts → jsx-runtime-CFq1K_Ve.d.ts} +11 -1
- package/dist/jsx-runtime.d.ts +1 -1
- package/dist/jsx-runtime.js +1 -1
- package/docs/actions.mdx +52 -1
- package/docs/adapters.mdx +72 -36
- package/docs/ai/ai-sdk-tools.mdx +227 -0
- package/docs/ai/index.mdx +63 -0
- package/docs/ai/meta.json +4 -0
- package/docs/{api → ai}/to-ai-messages.mdx +16 -3
- package/docs/ai/types.mdx +243 -0
- package/docs/api/cards.mdx +4 -0
- package/docs/api/chat.mdx +132 -10
- package/docs/api/index.mdx +6 -6
- package/docs/api/markdown.mdx +28 -5
- package/docs/api/message.mdx +54 -1
- package/docs/api/meta.json +1 -0
- package/docs/api/modals.mdx +50 -0
- package/docs/api/postable-message.mdx +58 -4
- package/docs/api/thread.mdx +11 -3
- package/docs/api/transcripts.mdx +220 -0
- package/docs/cards.mdx +6 -0
- package/docs/concurrency.mdx +58 -15
- package/docs/contributing/building.mdx +74 -2
- package/docs/contributing/testing.mdx +4 -0
- package/docs/conversation-history.mdx +137 -0
- package/docs/direct-messages.mdx +23 -5
- package/docs/ephemeral-messages.mdx +1 -1
- package/docs/error-handling.mdx +15 -3
- package/docs/files.mdx +21 -1
- package/docs/handling-events.mdx +10 -7
- package/docs/index.mdx +8 -5
- package/docs/meta.json +17 -3
- package/docs/modals.mdx +24 -0
- package/docs/posting-messages.mdx +10 -4
- package/docs/slash-commands.mdx +4 -4
- package/docs/streaming.mdx +75 -27
- package/docs/subject.mdx +53 -0
- package/docs/testing.mdx +142 -0
- package/docs/threads-messages-channels.mdx +10 -1
- package/docs/usage.mdx +15 -2
- package/package.json +23 -2
- package/resources/guides/how-to-build-an-ai-agent-for-slack-with-chat-sdk-and-ai-sdk.md +1 -1
package/docs/streaming.mdx
CHANGED
|
@@ -6,7 +6,7 @@ prerequisites:
|
|
|
6
6
|
- /docs/usage
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
Chat SDK accepts any `AsyncIterable<string>` as a message, enabling real-time streaming of AI responses and other incremental content to chat platforms. For platforms with native streaming support
|
|
9
|
+
Chat SDK accepts any `AsyncIterable<string>` as a message, enabling real-time streaming of AI responses and other incremental content to chat platforms. For platforms with native or structured streaming support, you can also stream `StreamChunk` objects for rich content like task progress cards and plan updates.
|
|
10
10
|
|
|
11
11
|
## AI SDK integration
|
|
12
12
|
|
|
@@ -59,9 +59,14 @@ await thread.post(stream);
|
|
|
59
59
|
| Platform | Method | Description |
|
|
60
60
|
|----------|--------|-------------|
|
|
61
61
|
| Slack | Native streaming API | Uses Slack's `chatStream` for smooth, real-time updates |
|
|
62
|
-
| Teams |
|
|
62
|
+
| Teams | Native (DMs) / Buffered (group chats) | Uses the Teams SDK's native `stream.emit()` for direct messages; accumulates chunks and posts one final message when no native streamer is active |
|
|
63
63
|
| Google Chat | Post + Edit | Posts a message then edits it as chunks arrive |
|
|
64
64
|
| Discord | Post + Edit | Posts a message then edits it as chunks arrive |
|
|
65
|
+
| Telegram | Post + Edit | Posts a message then edits it as chunks arrive |
|
|
66
|
+
| GitHub | Buffered | Accumulates chunks and posts one final comment |
|
|
67
|
+
| Linear | Agent sessions / Post + Edit | Uses agent session activities in agent-session threads; falls back to post+edit comments in issue threads |
|
|
68
|
+
| WhatsApp | Buffered | Accumulates chunks and sends one final message |
|
|
69
|
+
| Messenger | Buffered | Accumulates chunks and sends one final message |
|
|
65
70
|
|
|
66
71
|
The post+edit fallback throttles edits to avoid rate limits. Configure the update interval when creating your `Chat` instance:
|
|
67
72
|
|
|
@@ -104,9 +109,9 @@ When streaming content that contains GFM tables (e.g. from an LLM), the SDK auto
|
|
|
104
109
|
|
|
105
110
|
This happens transparently — no configuration needed.
|
|
106
111
|
|
|
107
|
-
## Structured streaming chunks
|
|
112
|
+
## Structured streaming chunks
|
|
108
113
|
|
|
109
|
-
For Slack
|
|
114
|
+
For Slack native streams and Linear agent-session streams, you can yield `StreamChunk` objects alongside plain text for rich progress updates:
|
|
110
115
|
|
|
111
116
|
```typescript title="lib/bot.ts" lineNumbers
|
|
112
117
|
import type { StreamChunk } from "chat";
|
|
@@ -144,39 +149,42 @@ await thread.post(stream);
|
|
|
144
149
|
| Type | Fields | Description |
|
|
145
150
|
|------|--------|-------------|
|
|
146
151
|
| `markdown_text` | `text` | Streamed text content |
|
|
147
|
-
| `task_update` | `id`, `title`, `status`, `details?`, `output?` | Tool/step progress
|
|
148
|
-
| `plan_update` | `title` | Plan title updates |
|
|
152
|
+
| `task_update` | `id`, `title`, `status`, `details?`, `output?` | Tool/step progress updates (`pending`, `in_progress`, `complete`, `error`) with optional extra task context |
|
|
153
|
+
| `plan_update` | `title` | Plan title updates on supported platforms |
|
|
149
154
|
|
|
150
|
-
###
|
|
155
|
+
### Streaming with options
|
|
151
156
|
|
|
152
|
-
|
|
157
|
+
Wrap a stream in a `StreamingPlan` to pass platform-specific options through `thread.post()` without dropping down to `adapter.stream()` directly:
|
|
153
158
|
|
|
154
159
|
```typescript
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
import { StreamingPlan } from "chat";
|
|
161
|
+
|
|
162
|
+
const planned = new StreamingPlan(stream, {
|
|
163
|
+
groupTasks: "plan", // Slack: render task cards as a single grouped block
|
|
164
|
+
endWith: [feedbackBlock], // Slack: Block Kit elements appended after stream stops
|
|
165
|
+
updateIntervalMs: 750, // Post+edit cadence on supported adapters
|
|
160
166
|
});
|
|
167
|
+
|
|
168
|
+
await thread.post(planned);
|
|
161
169
|
```
|
|
162
170
|
|
|
163
|
-
|
|
|
164
|
-
|
|
165
|
-
| `"timeline"`
|
|
166
|
-
| `
|
|
171
|
+
| Option | Platform | Description |
|
|
172
|
+
|--------|----------|-------------|
|
|
173
|
+
| `groupTasks` | Slack | `"timeline"` (default) renders task cards inline; `"plan"` groups them into one plan block |
|
|
174
|
+
| `endWith` | Slack | Block Kit elements attached when the stream stops (e.g. retry / feedback buttons) |
|
|
175
|
+
| `updateIntervalMs` | Post+edit adapters | Minimum interval between post+edit cycles in ms (default `500`) |
|
|
167
176
|
|
|
168
|
-
Adapters without structured chunk support extract text from `markdown_text` chunks and ignore other types.
|
|
177
|
+
Adapters without structured chunk support extract text from `markdown_text` chunks and ignore other types. Slack-only options are silently ignored on other platforms.
|
|
169
178
|
|
|
170
179
|
## Stop blocks (Slack only)
|
|
171
180
|
|
|
172
|
-
|
|
181
|
+
Use `endWith` on `StreamingPlan` to attach Block Kit elements to the final message. This is useful for adding action buttons after a streamed response completes:
|
|
173
182
|
|
|
174
183
|
```typescript title="lib/bot.ts" lineNumbers
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
stopBlocks: [
|
|
184
|
+
import { StreamingPlan } from "chat";
|
|
185
|
+
|
|
186
|
+
const planned = new StreamingPlan(textStream, {
|
|
187
|
+
endWith: [
|
|
180
188
|
{
|
|
181
189
|
type: "actions",
|
|
182
190
|
elements: [{
|
|
@@ -187,15 +195,55 @@ await thread.adapter.stream(thread.id, textStream, {
|
|
|
187
195
|
},
|
|
188
196
|
],
|
|
189
197
|
});
|
|
198
|
+
|
|
199
|
+
await thread.post(planned);
|
|
190
200
|
```
|
|
191
201
|
|
|
202
|
+
## Plan API
|
|
203
|
+
|
|
204
|
+
For step-by-step task progress that lives outside an LLM stream, post a `Plan` directly. `Plan` is a `PostableObject` you can mutate after posting — every mutation re-renders the block in place.
|
|
205
|
+
|
|
206
|
+
```typescript title="lib/bot.ts" lineNumbers
|
|
207
|
+
import { Plan } from "chat";
|
|
208
|
+
|
|
209
|
+
const plan = new Plan({ initialMessage: "Researching options..." });
|
|
210
|
+
await thread.post(plan);
|
|
211
|
+
|
|
212
|
+
const lookup = await plan.addTask({ title: "Look up customer record" });
|
|
213
|
+
// ...do work...
|
|
214
|
+
await plan.updateTask("Found 3 matches");
|
|
215
|
+
|
|
216
|
+
await plan.addTask({ title: "Summarize findings" });
|
|
217
|
+
await plan.complete({ completeMessage: "Done!" });
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
By default `updateTask()` mutates the most recent `in_progress` task. Pass `{ id }` to target a specific task — useful when work runs in parallel or out of order:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
const fetchTask = await plan.addTask({ title: "Fetch data" });
|
|
224
|
+
const transformTask = await plan.addTask({ title: "Transform" });
|
|
225
|
+
|
|
226
|
+
// Update a specific task by id, even if it isn't the most recent in_progress one.
|
|
227
|
+
await plan.updateTask({ id: fetchTask.id, output: "Got 42 rows" });
|
|
228
|
+
await plan.updateTask({ id: transformTask.id, status: "complete" });
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Adapters that don't support PostableObject editing (e.g. WhatsApp) render the plan as a fallback emoji-list message; the plan still posts, but mutations are no-ops.
|
|
232
|
+
|
|
233
|
+
| Method | Description |
|
|
234
|
+
|--------|-------------|
|
|
235
|
+
| `addTask({ title, children? })` | Append a new task. The previous in-progress task is auto-completed |
|
|
236
|
+
| `updateTask(input)` | Mutate the current (or `{ id }`-targeted) task's `output`, `status`, or `title` |
|
|
237
|
+
| `complete({ completeMessage })` | Mark all in-progress tasks complete and update the plan title |
|
|
238
|
+
| `reset({ initialMessage })` | Discard all tasks and start fresh with a new initial message — useful when re-using a plan handle for a new run |
|
|
239
|
+
|
|
192
240
|
## Streaming with conversation history
|
|
193
241
|
|
|
194
242
|
Combine message history with streaming for multi-turn AI conversations.
|
|
195
|
-
Use [`toAiMessages()`](/docs/
|
|
243
|
+
Use [`toAiMessages()`](/docs/ai/to-ai-messages) to convert chat messages into the `{ role, content }` format expected by AI SDKs:
|
|
196
244
|
|
|
197
245
|
```typescript title="lib/bot.ts" lineNumbers
|
|
198
|
-
import { toAiMessages } from "chat";
|
|
246
|
+
import { toAiMessages } from "chat/ai";
|
|
199
247
|
|
|
200
248
|
bot.onSubscribedMessage(async (thread, message) => {
|
|
201
249
|
// Fetch recent messages for context
|
|
@@ -208,4 +256,4 @@ bot.onSubscribedMessage(async (thread, message) => {
|
|
|
208
256
|
});
|
|
209
257
|
```
|
|
210
258
|
|
|
211
|
-
See the [`toAiMessages`
|
|
259
|
+
See the [`toAiMessages` reference](/docs/ai/to-ai-messages) for all options including `includeNames`, `transformMessage`, and attachment handling.
|
package/docs/subject.mdx
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Message Subject
|
|
3
|
+
description: Fetch the parent resource that a message is about.
|
|
4
|
+
type: guide
|
|
5
|
+
prerequisites:
|
|
6
|
+
- /docs/handling-events
|
|
7
|
+
related:
|
|
8
|
+
- /docs/conversation-history
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
When your bot receives a comment on a Linear issue or GitHub PR, `message.subject` resolves the parent resource so your handler knows what the conversation is about.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```typescript title="lib/bot.ts" lineNumbers
|
|
16
|
+
bot.onNewMention(async (thread, message) => {
|
|
17
|
+
const subject = await message.subject;
|
|
18
|
+
|
|
19
|
+
if (subject) {
|
|
20
|
+
await thread.post(
|
|
21
|
+
`This is about: ${subject.title} (${subject.status})\n${subject.url}`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
On Linear and GitHub, comment webhooks deliver the comment text but not the parent issue or pull request — `message.subject` fetches it from the platform API on first access. The result is cached on the message instance. On chat platforms (which have no parent-resource concept), or if the API call fails, it returns `null`.
|
|
28
|
+
|
|
29
|
+
See [`MessageSubject`](/docs/api/message#messagesubject) for the full type shape.
|
|
30
|
+
|
|
31
|
+
### Platform support
|
|
32
|
+
|
|
33
|
+
| Platform | `message.subject` returns |
|
|
34
|
+
|----------|--------------------------|
|
|
35
|
+
| Linear | Parent issue (from comment webhooks) |
|
|
36
|
+
| GitHub | Parent issue or PR (from comment webhooks) |
|
|
37
|
+
|
|
38
|
+
All other platforms return `null`.
|
|
39
|
+
|
|
40
|
+
## User info
|
|
41
|
+
|
|
42
|
+
For user profile details, use [`bot.getUser`](/docs/api/chat#getuser):
|
|
43
|
+
|
|
44
|
+
```typescript title="lib/bot.ts" lineNumbers
|
|
45
|
+
bot.onNewMention(async (thread, message) => {
|
|
46
|
+
const user = await bot.getUser(message.author);
|
|
47
|
+
if (user) {
|
|
48
|
+
await thread.post(`Hi ${user.fullName} (${user.email})`);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
For anything beyond `message.subject`, access the platform's typed API client via [`bot.getAdapter("github").octokit`](/docs/api/chat#getadapter) or [`bot.getAdapter("linear").linearClient`](/docs/api/chat#getadapter).
|
package/docs/testing.mdx
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Testing
|
|
3
|
+
description: Test your bot handlers and custom adapters with @chat-adapter/tests — Vitest factories, custom matchers, and a setup file.
|
|
4
|
+
type: guide
|
|
5
|
+
prerequisites:
|
|
6
|
+
- /docs/getting-started
|
|
7
|
+
related:
|
|
8
|
+
- /docs/state
|
|
9
|
+
- /docs/handling-events
|
|
10
|
+
- /docs/contributing/testing
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
The [`@chat-adapter/tests`](https://www.npmjs.com/package/@chat-adapter/tests) package gives you Vitest factories, custom matchers, and a setup file for testing bots and custom adapters built on Chat SDK.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add -D @chat-adapter/tests
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`chat` and `vitest` are peer dependencies — they should already be in your project.
|
|
22
|
+
|
|
23
|
+
## Setup file (recommended)
|
|
24
|
+
|
|
25
|
+
Auto-register all matchers by adding the package's setup file to your Vitest config:
|
|
26
|
+
|
|
27
|
+
```typescript title="vitest.config.ts" lineNumbers
|
|
28
|
+
import { defineConfig } from "vitest/config";
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
test: {
|
|
32
|
+
setupFiles: ["@chat-adapter/tests/setup"],
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Without the setup file, register matchers manually:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { matchers } from "@chat-adapter/tests/matchers";
|
|
41
|
+
expect.extend(matchers);
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Mock factories
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import {
|
|
48
|
+
createMockAdapter,
|
|
49
|
+
createMockChatInstance,
|
|
50
|
+
createMockState,
|
|
51
|
+
createTestMessage,
|
|
52
|
+
mockLogger,
|
|
53
|
+
} from "@chat-adapter/tests";
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Factory | Returns | Notes |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `createMockAdapter(name?, overrides?)` | `Adapter` | Every method is `vi.fn()` with sensible defaults |
|
|
59
|
+
| `createMockChatInstance(options?)` | `ChatInstance` | Every `process*` handler is `vi.fn()`; `getState`/`getUserName`/`getLogger` wired up |
|
|
60
|
+
| `createMockState()` | `MockStateAdapter` | In-memory `Map`s for subscriptions, locks, KV, lists, queues; `cache` exposes the underlying map |
|
|
61
|
+
| `createTestMessage(id, text, overrides?)` | `Message` | Markdown text is parsed into the formatted AST |
|
|
62
|
+
| `mockLogger` / `createMockLogger()` | `Logger` | Shared default vs fresh-per-call |
|
|
63
|
+
|
|
64
|
+
## Matchers
|
|
65
|
+
|
|
66
|
+
| Matcher | Asserts |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `expect(adapter).toHavePosted(threadId, textPattern?)` | `adapter.postMessage` was called for this thread |
|
|
69
|
+
| `expect(adapter).toHaveEdited(threadId, messageId, textPattern?)` | `adapter.editMessage` was called for this message |
|
|
70
|
+
| `expect(adapter).toHaveDeleted(threadId, messageId)` | `adapter.deleteMessage` was called for this message |
|
|
71
|
+
| `expect(adapter).toHaveReactedWith(threadId, messageId, emoji)` | `adapter.addReaction` was called with the emoji (string or `EmojiValue.name`) |
|
|
72
|
+
| `expect(adapter).toHaveStartedTyping(threadId)` | `adapter.startTyping` was called for this thread |
|
|
73
|
+
| `expect(adapter).toHavePostedToChannel(channelId, textPattern?)` | `adapter.postChannelMessage` was called for this channel |
|
|
74
|
+
| `expect(chat).toHaveDispatched(handler)` | The named `process*` handler on the mock `ChatInstance` was called |
|
|
75
|
+
| `expect(state).toBeSubscribedTo(threadId)` | `state.isSubscribed(threadId)` resolves to `true` (async — `await expect(...)`) |
|
|
76
|
+
|
|
77
|
+
Text-pattern matchers extract a comparable string from `AdapterPostableMessage` — strings directly, `PostableMarkdown.markdown`, `PostableRaw.raw`, and `PostableCard.fallbackText`. AST-shaped messages and cards without `fallbackText` aren't text-matchable; assert without `textPattern` and inspect `mock.calls` directly.
|
|
78
|
+
|
|
79
|
+
## Bot authors: test your handlers
|
|
80
|
+
|
|
81
|
+
When you're building a bot on top of Chat SDK, the kit lets you exercise your handlers without a real Slack/Teams/etc. webhook on the wire:
|
|
82
|
+
|
|
83
|
+
```typescript title="bot.test.ts"
|
|
84
|
+
import { describe, expect, it } from "vitest";
|
|
85
|
+
import { Chat } from "chat";
|
|
86
|
+
import { createMockAdapter, createMockState } from "@chat-adapter/tests";
|
|
87
|
+
|
|
88
|
+
describe("bot handlers", () => {
|
|
89
|
+
it("replies with a greeting on mention", async () => {
|
|
90
|
+
const slack = createMockAdapter("slack");
|
|
91
|
+
const state = createMockState();
|
|
92
|
+
const bot = new Chat({
|
|
93
|
+
userName: "mybot",
|
|
94
|
+
adapters: { slack },
|
|
95
|
+
state,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
bot.onNewMention(async (thread) => {
|
|
99
|
+
await thread.post("hello there");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Drive a synthesized mention through the bot…
|
|
103
|
+
// (use your adapter's webhook path or a thread-level call)
|
|
104
|
+
|
|
105
|
+
expect(slack).toHavePosted("slack:C1:t1", /hello there/);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Adapter authors: test webhook → dispatch
|
|
111
|
+
|
|
112
|
+
When you're building a custom `Adapter`, the kit gives you a `ChatInstance` mock you can hand to your adapter and assert that webhooks route through the right `process*` hook with the right normalized payload:
|
|
113
|
+
|
|
114
|
+
```typescript title="adapter.test.ts"
|
|
115
|
+
import { describe, expect, it } from "vitest";
|
|
116
|
+
import { createMockChatInstance } from "@chat-adapter/tests";
|
|
117
|
+
import { MyAdapter } from "./adapter";
|
|
118
|
+
|
|
119
|
+
describe("MyAdapter.handleWebhook", () => {
|
|
120
|
+
it("dispatches incoming messages through processMessage", async () => {
|
|
121
|
+
const chat = createMockChatInstance();
|
|
122
|
+
const adapter = new MyAdapter({ /* config */ });
|
|
123
|
+
await adapter.initialize(chat);
|
|
124
|
+
|
|
125
|
+
const request = new Request("https://example.com/webhook", {
|
|
126
|
+
method: "POST",
|
|
127
|
+
body: JSON.stringify({ /* platform-specific payload */ }),
|
|
128
|
+
headers: { "content-type": "application/json" },
|
|
129
|
+
});
|
|
130
|
+
const response = await adapter.handleWebhook(request);
|
|
131
|
+
|
|
132
|
+
expect(response.status).toBe(200);
|
|
133
|
+
expect(chat).toHaveDispatched("processMessage");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Adapter-specific helpers
|
|
139
|
+
|
|
140
|
+
Helpers that depend on a specific platform's wire format (signed Slack webhooks, Teams claim builders, etc.) live in each adapter's own `/testing` subpath rather than in this kit, so adopting `@chat-adapter/tests` doesn't pull in adapter dependencies you don't use.
|
|
141
|
+
|
|
142
|
+
If you're contributing adapters or core to this repo, see the [Testing adapters contributing guide](/docs/contributing/testing) for hand-rolled patterns used inside `packages/`.
|
|
@@ -13,6 +13,15 @@ related:
|
|
|
13
13
|
|
|
14
14
|
A `Thread` represents a conversation thread on any platform. It provides methods for posting messages, managing subscriptions, and accessing message history.
|
|
15
15
|
|
|
16
|
+
Thread instances are most often supplied by the SDK to your event handlers. You can also construct one explicitly from a thread ID — useful for cron jobs, workflow steps, or any other context outside an inbound webhook:
|
|
17
|
+
|
|
18
|
+
```typescript title="lib/bot.ts" lineNumbers
|
|
19
|
+
const thread = bot.thread("slack:C123ABC:1234567890.123456");
|
|
20
|
+
await thread.post("Reminder from a cron job");
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
For DM-style conversations, use [`bot.openDM(userIdOrAuthor)`](/docs/direct-messages) instead. It resolves the right channel and thread for user ID formats the SDK can infer.
|
|
24
|
+
|
|
16
25
|
### Post a message
|
|
17
26
|
|
|
18
27
|
```typescript title="lib/bot.ts" lineNumbers
|
|
@@ -31,7 +40,7 @@ await thread.post({
|
|
|
31
40
|
|
|
32
41
|
### Subscribe and unsubscribe
|
|
33
42
|
|
|
34
|
-
Subscriptions persist across restarts (stored in your state adapter). When a thread is subscribed, all messages route to `onSubscribedMessage`.
|
|
43
|
+
Subscriptions persist across restarts (stored in your state adapter). When a non-DM thread is subscribed, all messages route to `onSubscribedMessage`. DM threads route to `onDirectMessage` first when a direct message handler is registered.
|
|
35
44
|
|
|
36
45
|
```typescript title="lib/bot.ts" lineNumbers
|
|
37
46
|
await thread.subscribe();
|
package/docs/usage.mdx
CHANGED
|
@@ -34,7 +34,7 @@ bot.onNewMention(async (thread) => {
|
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
<Callout type="info">
|
|
37
|
-
This example uses Redis. Chat SDK also supports [PostgreSQL](/
|
|
37
|
+
This example uses Redis. Chat SDK also supports [PostgreSQL](/adapters/official/postgres) and [ioredis](/adapters/official/ioredis) as production state adapters. See [State Adapters](/docs/state) for all options.
|
|
38
38
|
</Callout>
|
|
39
39
|
|
|
40
40
|
Each adapter factory auto-detects credentials from environment variables (`SLACK_BOT_TOKEN`, `SLACK_SIGNING_SECRET`, `REDIS_URL`, etc.), so you can get started with zero config. Pass explicit values to override.
|
|
@@ -72,6 +72,7 @@ Your event handlers work identically across all registered adapters — the SDK
|
|
|
72
72
|
| `state` | `StateAdapter` | *required* | State adapter for subscriptions and locking |
|
|
73
73
|
| `logger` | `Logger \| LogLevel` | `"info"` | Logger instance or log level (`"debug"`, `"info"`, `"warn"`, `"error"`, `"silent"`) |
|
|
74
74
|
| `dedupeTtlMs` | `number` | `300000` | TTL in ms for message deduplication (5 minutes) |
|
|
75
|
+
| `concurrency` | `"drop" \| "queue" \| "debounce" \| "burst" \| "concurrent" \| ConcurrencyConfig` | `"drop"` | Strategy for overlapping messages on the same thread |
|
|
75
76
|
| `streamingUpdateIntervalMs` | `number` | `500` | Update interval in ms for post+edit streaming |
|
|
76
77
|
| `fallbackStreamingPlaceholderText` | `string \| null` | `"..."` | Placeholder text while streaming starts. Set to `null` to skip |
|
|
77
78
|
| `onLockConflict` | `'drop' \| 'force' \| (threadId, message) => 'drop' \| 'force'` | `"drop"` | Behavior when a thread lock is already held. `'force'` releases the existing lock and re-acquires it, enabling interrupt/steerability for long-running handlers |
|
|
@@ -89,6 +90,18 @@ await slack.setSuggestedPrompts(channelId, threadTs, [
|
|
|
89
90
|
]);
|
|
90
91
|
```
|
|
91
92
|
|
|
93
|
+
For typed access to the platform's native API client, use the SDK-named getter on each adapter:
|
|
94
|
+
|
|
95
|
+
```typescript title="lib/bot.ts" lineNumbers
|
|
96
|
+
const slack = bot.getAdapter("slack").webClient; // WebClient
|
|
97
|
+
const linear = bot.getAdapter("linear").linearClient; // LinearClient
|
|
98
|
+
const github = bot.getAdapter("github").octokit; // Octokit
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The previous `.client` getter still works as a deprecated alias on all three adapters.
|
|
102
|
+
|
|
103
|
+
See [`getAdapter`](/docs/api/chat#getadapter) for multi-tenant constraints.
|
|
104
|
+
|
|
92
105
|
## Webhook routing
|
|
93
106
|
|
|
94
107
|
The `webhooks` property provides type-safe handlers for each registered adapter. Wire these up to your HTTP framework's routes:
|
|
@@ -139,7 +152,7 @@ const bot = Chat.getSingleton();
|
|
|
139
152
|
Open a DM thread with a user by passing their platform user ID or an `Author` object:
|
|
140
153
|
|
|
141
154
|
```typescript title="lib/bot.ts" lineNumbers
|
|
142
|
-
const dm = await bot.openDM("
|
|
155
|
+
const dm = await bot.openDM("U123ABC");
|
|
143
156
|
await dm.post("Hey! Just wanted to follow up on your request.");
|
|
144
157
|
```
|
|
145
158
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chat",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.29.0",
|
|
4
4
|
"description": "Unified chat abstraction for Slack, Teams, Google Chat, and Discord",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20"
|
|
8
|
+
},
|
|
6
9
|
"main": "./dist/index.js",
|
|
7
10
|
"module": "./dist/index.js",
|
|
8
11
|
"types": "./dist/index.d.ts",
|
|
@@ -11,6 +14,10 @@
|
|
|
11
14
|
"types": "./dist/index.d.ts",
|
|
12
15
|
"import": "./dist/index.js"
|
|
13
16
|
},
|
|
17
|
+
"./ai": {
|
|
18
|
+
"types": "./dist/ai/index.d.ts",
|
|
19
|
+
"import": "./dist/ai/index.js"
|
|
20
|
+
},
|
|
14
21
|
"./jsx-runtime": {
|
|
15
22
|
"types": "./dist/jsx-runtime.d.ts",
|
|
16
23
|
"import": "./dist/jsx-runtime.js"
|
|
@@ -37,9 +44,23 @@
|
|
|
37
44
|
"devDependencies": {
|
|
38
45
|
"@types/mdast": "^4.0.4",
|
|
39
46
|
"@types/node": "^25.3.2",
|
|
47
|
+
"ai": "^6.0.182",
|
|
40
48
|
"tsup": "^8.3.5",
|
|
41
49
|
"typescript": "^5.7.2",
|
|
42
|
-
"vitest": "^4.0.18"
|
|
50
|
+
"vitest": "^4.0.18",
|
|
51
|
+
"zod": "^4.3.6"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"ai": "^6.0.182",
|
|
55
|
+
"zod": "^3.0.0 || ^4.0.0"
|
|
56
|
+
},
|
|
57
|
+
"peerDependenciesMeta": {
|
|
58
|
+
"ai": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"zod": {
|
|
62
|
+
"optional": true
|
|
63
|
+
}
|
|
43
64
|
},
|
|
44
65
|
"repository": {
|
|
45
66
|
"type": "git",
|
|
@@ -86,7 +86,7 @@ Each tool has a `description` (which tells the model when to use it), an `inputS
|
|
|
86
86
|
|
|
87
87
|
Create `lib/bot.ts` with a `ToolLoopAgent` and a `Chat` instance:
|
|
88
88
|
|
|
89
|
-
`import { Chat } from "chat"; import { toAiMessages } from "chat"; import { createSlackAdapter } from "@chat-adapter/slack"; import { createRedisState } from "@chat-adapter/state-redis"; import { ToolLoopAgent } from "ai"; import { tools } from "./tools"; const agent = new ToolLoopAgent({ model: "anthropic/claude-sonnet-4.6", instructions: "You are a helpful AI assistant in a Slack workspace. " + "Answer questions clearly and use your tools when you need " + "real-time data. Keep responses concise and well-formatted for chat.", tools, }); export const bot = new Chat({ userName: "ai-agent", adapters: { slack: createSlackAdapter(), }, state: createRedisState(), }); // Handle first-time mentions bot.onNewMention(async (thread, message) => { await thread.subscribe(); const result = await agent.stream({ prompt: message.text }); await thread.post(result.fullStream); }); // Handle follow-up messages in subscribed threads bot.onSubscribedMessage(async (thread, message) => { const allMessages = []; for await (const msg of thread.allMessages) { allMessages.push(msg); } const history = await toAiMessages(allMessages); const result = await agent.stream({ messages: history }); await thread.post(result.fullStream); });`
|
|
89
|
+
`import { Chat } from "chat"; import { toAiMessages } from "chat/ai"; import { createSlackAdapter } from "@chat-adapter/slack"; import { createRedisState } from "@chat-adapter/state-redis"; import { ToolLoopAgent } from "ai"; import { tools } from "./tools"; const agent = new ToolLoopAgent({ model: "anthropic/claude-sonnet-4.6", instructions: "You are a helpful AI assistant in a Slack workspace. " + "Answer questions clearly and use your tools when you need " + "real-time data. Keep responses concise and well-formatted for chat.", tools, }); export const bot = new Chat({ userName: "ai-agent", adapters: { slack: createSlackAdapter(), }, state: createRedisState(), }); // Handle first-time mentions bot.onNewMention(async (thread, message) => { await thread.subscribe(); const result = await agent.stream({ prompt: message.text }); await thread.post(result.fullStream); }); // Handle follow-up messages in subscribed threads bot.onSubscribedMessage(async (thread, message) => { const allMessages = []; for await (const msg of thread.allMessages) { allMessages.push(msg); } const history = await toAiMessages(allMessages); const result = await agent.stream({ messages: history }); await thread.post(result.fullStream); });`
|
|
90
90
|
|
|
91
91
|
When someone @mentions the bot, `onNewMention` fires. The handler subscribes to the thread (to track future messages in that thread) and streams the agent's response. For follow-up messages, `onSubscribedMessage` retrieves the full thread history using `thread.allMessages`, converts it to the AI SDK message format with `toAiMessages`and passes it to the agent so it has a complete conversation context.
|
|
92
92
|
|