chat 4.27.0 → 4.28.1
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-AN7MRAVW.js → chunk-V25FKIIL.js} +5 -1
- package/dist/index.d.ts +269 -31
- package/dist/index.js +544 -88
- package/dist/{jsx-runtime-Co9uV6l7.d.ts → jsx-runtime-DxGwoLu2.d.ts} +10 -0
- 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 +42 -36
- package/docs/api/cards.mdx +4 -0
- package/docs/api/chat.mdx +91 -9
- package/docs/api/index.mdx +2 -0
- package/docs/api/markdown.mdx +28 -5
- package/docs/api/message.mdx +53 -0
- package/docs/api/meta.json +2 -0
- package/docs/api/modals.mdx +50 -0
- package/docs/api/postable-message.mdx +55 -1
- package/docs/api/thread.mdx +10 -2
- package/docs/api/transcripts.mdx +220 -0
- package/docs/cards.mdx +6 -0
- package/docs/concurrency.mdx +4 -0
- package/docs/contributing/building.mdx +73 -1
- package/docs/conversation-history.mdx +137 -0
- package/docs/direct-messages.mdx +13 -4
- package/docs/ephemeral-messages.mdx +1 -1
- package/docs/error-handling.mdx +15 -3
- package/docs/files.mdx +1 -1
- package/docs/index.mdx +7 -5
- package/docs/meta.json +14 -3
- package/docs/modals.mdx +24 -0
- package/docs/posting-messages.mdx +7 -3
- package/docs/streaming.mdx +72 -24
- package/docs/subject.mdx +53 -0
- package/docs/threads-messages-channels.mdx +9 -0
- package/docs/usage.mdx +11 -2
- package/package.json +1 -1
|
@@ -7,9 +7,14 @@ type: reference
|
|
|
7
7
|
`PostableMessage` is the union of all message formats accepted by `thread.post()` and `sent.edit()`.
|
|
8
8
|
|
|
9
9
|
```typescript
|
|
10
|
-
type PostableMessage =
|
|
10
|
+
type PostableMessage =
|
|
11
|
+
| AdapterPostableMessage
|
|
12
|
+
| AsyncIterable<string | StreamChunk | StreamEvent>
|
|
13
|
+
| PostableObject;
|
|
11
14
|
```
|
|
12
15
|
|
|
16
|
+
`PostableObject` covers `Plan` (mutable task lists) and `StreamingPlan` (streams with platform-specific options) — both documented below.
|
|
17
|
+
|
|
13
18
|
## String
|
|
14
19
|
|
|
15
20
|
Raw text passed through as-is to the platform.
|
|
@@ -133,6 +138,55 @@ await thread.post({
|
|
|
133
138
|
}}
|
|
134
139
|
/>
|
|
135
140
|
|
|
141
|
+
## Plan
|
|
142
|
+
|
|
143
|
+
A `Plan` is a step-by-step task list that mutates after posting. Each `addTask` / `updateTask` / `complete` call re-renders the same message in place. See [Plan API](/docs/streaming#plan-api) for full usage.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { Plan } from "chat";
|
|
147
|
+
|
|
148
|
+
const plan = new Plan({ initialMessage: "Researching options..." });
|
|
149
|
+
await thread.post(plan);
|
|
150
|
+
await plan.addTask({ title: "Look up records" });
|
|
151
|
+
await plan.complete({ completeMessage: "Done!" });
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Adapters that don't support `PostableObject` editing render the plan as fallback text and ignore subsequent mutations.
|
|
155
|
+
|
|
156
|
+
## StreamingPlan
|
|
157
|
+
|
|
158
|
+
Wraps an async iterable with platform-specific streaming options. Use this when you need to pass options like task grouping or stop blocks through `thread.post()`. See [Streaming with options](/docs/streaming#streaming-with-options).
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { StreamingPlan } from "chat";
|
|
162
|
+
|
|
163
|
+
const planned = new StreamingPlan(stream, {
|
|
164
|
+
groupTasks: "plan",
|
|
165
|
+
endWith: [feedbackBlock],
|
|
166
|
+
updateIntervalMs: 750,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await thread.post(planned);
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
<TypeTable
|
|
173
|
+
type={{
|
|
174
|
+
groupTasks: {
|
|
175
|
+
description: 'Slack: render task_update chunks as `"plan"` (single grouped block) or `"timeline"` (inline cards, default).',
|
|
176
|
+
type: '"plan" | "timeline" | undefined',
|
|
177
|
+
},
|
|
178
|
+
endWith: {
|
|
179
|
+
description: 'Slack: Block Kit elements appended when the stream stops (e.g. retry / feedback buttons).',
|
|
180
|
+
type: 'unknown[] | undefined',
|
|
181
|
+
},
|
|
182
|
+
updateIntervalMs: {
|
|
183
|
+
description: 'Post+edit adapters: minimum interval between update cycles in ms.',
|
|
184
|
+
type: 'number | undefined',
|
|
185
|
+
default: '500',
|
|
186
|
+
},
|
|
187
|
+
}}
|
|
188
|
+
/>
|
|
189
|
+
|
|
136
190
|
## AsyncIterable (streaming)
|
|
137
191
|
|
|
138
192
|
An async iterable of strings, `StreamChunk` objects, or stream events. The SDK streams the message in real time using platform-native APIs where available.
|
package/docs/api/thread.mdx
CHANGED
|
@@ -39,7 +39,7 @@ A `Thread` is provided to your event handlers and represents a conversation thre
|
|
|
39
39
|
|
|
40
40
|
## post
|
|
41
41
|
|
|
42
|
-
Post a message to the thread. Accepts strings, structured messages, cards, and
|
|
42
|
+
Post a message to the thread. Accepts strings, structured messages, cards, streams, and `PostableObject` instances (`Plan`, `StreamingPlan`).
|
|
43
43
|
|
|
44
44
|
```typescript
|
|
45
45
|
// Plain text
|
|
@@ -56,11 +56,19 @@ await thread.post(Card({ title: "Hi", children: [Text("Hello")] }));
|
|
|
56
56
|
|
|
57
57
|
// Stream (fullStream recommended for multi-step agents)
|
|
58
58
|
await thread.post(result.fullStream);
|
|
59
|
+
|
|
60
|
+
// Plan (mutable task list)
|
|
61
|
+
const plan = new Plan({ initialMessage: "Working..." });
|
|
62
|
+
await thread.post(plan);
|
|
63
|
+
await plan.addTask({ title: "Step 1" });
|
|
64
|
+
|
|
65
|
+
// Streaming with platform options
|
|
66
|
+
await thread.post(new StreamingPlan(stream, { groupTasks: "plan" }));
|
|
59
67
|
```
|
|
60
68
|
|
|
61
69
|
**Parameters:** `message: string | PostableMessage | CardJSXElement`
|
|
62
70
|
|
|
63
|
-
**Returns:** `Promise<SentMessage>` —
|
|
71
|
+
**Returns:** `Promise<SentMessage | PostableObject>` — for plain messages and streams, a `SentMessage` with `edit()`, `delete()`, `addReaction()`, and `removeReaction()` methods; for `Plan` / `StreamingPlan` inputs, the same object is returned so you can keep mutating it.
|
|
64
72
|
|
|
65
73
|
See [Posting Messages](/docs/posting-messages) for details on each format.
|
|
66
74
|
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Transcripts
|
|
3
|
+
description: Cross-platform per-user transcript persistence — configuration, methods, and entry shape.
|
|
4
|
+
type: reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
`bot.transcripts` provides per-user message persistence keyed by a stable cross-platform identifier. See the [Conversation history](/docs/conversation-history) guide for usage patterns.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { Chat } from "chat";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Configuration
|
|
14
|
+
|
|
15
|
+
`transcripts` and `identity` are configured on `ChatConfig`. Both must be set together — passing `transcripts` without `identity` throws at construction.
|
|
16
|
+
|
|
17
|
+
### ChatConfig.transcripts
|
|
18
|
+
|
|
19
|
+
<TypeTable
|
|
20
|
+
type={{
|
|
21
|
+
retention: {
|
|
22
|
+
description: 'List TTL, refreshed on every append. Accepts ms or a duration string ("45s", "30m", "6h", "7d"). Omit for no expiry.',
|
|
23
|
+
type: 'number | DurationString | undefined',
|
|
24
|
+
},
|
|
25
|
+
maxPerUser: {
|
|
26
|
+
description: 'Hard cap per user. Older entries are evicted on append.',
|
|
27
|
+
type: 'number',
|
|
28
|
+
default: '200',
|
|
29
|
+
},
|
|
30
|
+
storeFormatted: {
|
|
31
|
+
description: 'Persist the mdast `formatted` field alongside `text`. Off by default to keep storage small.',
|
|
32
|
+
type: 'boolean',
|
|
33
|
+
default: 'false',
|
|
34
|
+
},
|
|
35
|
+
}}
|
|
36
|
+
/>
|
|
37
|
+
|
|
38
|
+
### ChatConfig.identity
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
identity: (context: IdentityContext) => string | null | Promise<string | null>;
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Called once per inbound message during dispatch. The result is attached to the `Message` instance as `message.userKey`. Return `null` to skip persistence for an event.
|
|
45
|
+
|
|
46
|
+
#### IdentityContext
|
|
47
|
+
|
|
48
|
+
<TypeTable
|
|
49
|
+
type={{
|
|
50
|
+
adapter: {
|
|
51
|
+
description: 'Adapter name (e.g. "slack", "discord").',
|
|
52
|
+
type: 'string',
|
|
53
|
+
},
|
|
54
|
+
author: {
|
|
55
|
+
description: 'Message author info.',
|
|
56
|
+
type: 'Author',
|
|
57
|
+
},
|
|
58
|
+
message: {
|
|
59
|
+
description: 'The inbound message.',
|
|
60
|
+
type: 'Message',
|
|
61
|
+
},
|
|
62
|
+
}}
|
|
63
|
+
/>
|
|
64
|
+
|
|
65
|
+
## Methods
|
|
66
|
+
|
|
67
|
+
Access via `bot.transcripts`. Throws if `transcripts` was not configured on the `Chat` instance.
|
|
68
|
+
|
|
69
|
+
### append
|
|
70
|
+
|
|
71
|
+
Persist a `Message` (typically the inbound user message) or an `AppendInput` (typically a bot reply you just posted).
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
append(
|
|
75
|
+
thread: Postable,
|
|
76
|
+
message: Message | AppendInput,
|
|
77
|
+
options?: AppendOptions,
|
|
78
|
+
): Promise<TranscriptEntry | null>;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
When `message` is a `Message`, `userKey` is read from the instance. If it's `undefined` (the resolver returned `null`), the call is a no-op and returns `null`. When `message` is an `AppendInput`, `options.userKey` is required.
|
|
82
|
+
|
|
83
|
+
#### AppendInput
|
|
84
|
+
|
|
85
|
+
<TypeTable
|
|
86
|
+
type={{
|
|
87
|
+
role: {
|
|
88
|
+
description: 'Role tag for the entry.',
|
|
89
|
+
type: '"user" | "assistant" | "system"',
|
|
90
|
+
},
|
|
91
|
+
text: {
|
|
92
|
+
description: 'Plain-text body.',
|
|
93
|
+
type: 'string',
|
|
94
|
+
},
|
|
95
|
+
formatted: {
|
|
96
|
+
description: 'Optional mdast AST. Only stored when `transcripts.storeFormatted` is true.',
|
|
97
|
+
type: 'FormattedContent | undefined',
|
|
98
|
+
},
|
|
99
|
+
platformMessageId: {
|
|
100
|
+
description: 'Platform-native message ID, when known.',
|
|
101
|
+
type: 'string | undefined',
|
|
102
|
+
},
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
#### AppendOptions
|
|
107
|
+
|
|
108
|
+
<TypeTable
|
|
109
|
+
type={{
|
|
110
|
+
userKey: {
|
|
111
|
+
description: 'Required when appending an `AppendInput` (assistant or system role); ignored when appending a `Message`.',
|
|
112
|
+
type: 'string | undefined',
|
|
113
|
+
},
|
|
114
|
+
}}
|
|
115
|
+
/>
|
|
116
|
+
|
|
117
|
+
### list
|
|
118
|
+
|
|
119
|
+
Returns entries in chronological order (oldest first). When `limit` is set, returns the newest `N` entries — still chronologically.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
list(query: ListQuery): Promise<TranscriptEntry[]>;
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### ListQuery
|
|
126
|
+
|
|
127
|
+
<TypeTable
|
|
128
|
+
type={{
|
|
129
|
+
userKey: {
|
|
130
|
+
description: 'Cross-platform user key.',
|
|
131
|
+
type: 'string',
|
|
132
|
+
},
|
|
133
|
+
limit: {
|
|
134
|
+
description: 'Maximum entries returned. Cannot exceed `maxPerUser` because that is the storage cap.',
|
|
135
|
+
type: 'number',
|
|
136
|
+
default: '50',
|
|
137
|
+
},
|
|
138
|
+
platforms: {
|
|
139
|
+
description: 'Filter to a subset of adapter names.',
|
|
140
|
+
type: 'string[] | undefined',
|
|
141
|
+
},
|
|
142
|
+
threadId: {
|
|
143
|
+
description: 'Filter to a single thread.',
|
|
144
|
+
type: 'string | undefined',
|
|
145
|
+
},
|
|
146
|
+
roles: {
|
|
147
|
+
description: 'Filter to specific roles.',
|
|
148
|
+
type: '("user" | "assistant" | "system")[] | undefined',
|
|
149
|
+
},
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
|
|
153
|
+
### count
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
count(query: CountQuery): Promise<number>;
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Returns the total number of entries stored under the user key. `CountQuery` has a single field, `userKey: string`.
|
|
160
|
+
|
|
161
|
+
### delete
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
delete(target: { userKey: string }): Promise<{ deleted: number }>;
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Wipes every entry stored under the user key. Returns the count that was removed. Single-entry and time-range deletes are not supported — the underlying `appendToList` primitive can't support them safely under concurrent writes.
|
|
168
|
+
|
|
169
|
+
## TranscriptEntry
|
|
170
|
+
|
|
171
|
+
Returned by `append` and `list`.
|
|
172
|
+
|
|
173
|
+
<TypeTable
|
|
174
|
+
type={{
|
|
175
|
+
id: {
|
|
176
|
+
description: 'UUID assigned by the SDK at append time.',
|
|
177
|
+
type: 'string',
|
|
178
|
+
},
|
|
179
|
+
userKey: {
|
|
180
|
+
description: 'Cross-platform user key from the IdentityResolver.',
|
|
181
|
+
type: 'string',
|
|
182
|
+
},
|
|
183
|
+
role: {
|
|
184
|
+
description: 'Role tag.',
|
|
185
|
+
type: '"user" | "assistant" | "system"',
|
|
186
|
+
},
|
|
187
|
+
text: {
|
|
188
|
+
description: 'Plain-text body — canonical for prompt building.',
|
|
189
|
+
type: 'string',
|
|
190
|
+
},
|
|
191
|
+
formatted: {
|
|
192
|
+
description: 'mdast AST. Only present when `transcripts.storeFormatted` is true.',
|
|
193
|
+
type: 'FormattedContent | undefined',
|
|
194
|
+
},
|
|
195
|
+
platform: {
|
|
196
|
+
description: 'Originating adapter name.',
|
|
197
|
+
type: 'string',
|
|
198
|
+
},
|
|
199
|
+
threadId: {
|
|
200
|
+
description: 'Originating thread ID.',
|
|
201
|
+
type: 'string',
|
|
202
|
+
},
|
|
203
|
+
platformMessageId: {
|
|
204
|
+
description: 'Platform-native message ID, when known.',
|
|
205
|
+
type: 'string | undefined',
|
|
206
|
+
},
|
|
207
|
+
timestamp: {
|
|
208
|
+
description: 'ms-since-epoch, set at append time on the SDK side.',
|
|
209
|
+
type: 'number',
|
|
210
|
+
},
|
|
211
|
+
}}
|
|
212
|
+
/>
|
|
213
|
+
|
|
214
|
+
## Storage
|
|
215
|
+
|
|
216
|
+
Backed by `StateAdapter.appendToList` / `getList` / `delete`. Every built-in state adapter (`memory`, `redis`, `ioredis`, `pg`) supports these primitives.
|
|
217
|
+
|
|
218
|
+
Entries are stored under `transcripts:user:{userKey}` as a capped list. `appendToList` is atomic, so concurrent inbound messages don't race.
|
|
219
|
+
|
|
220
|
+
The `retention` value is applied as the list TTL and refreshed on every append. With `retention: "30d"`, a user who hasn't talked to the bot in 30 days has their transcript expire automatically.
|
package/docs/cards.mdx
CHANGED
|
@@ -115,6 +115,12 @@ Set `actionType="modal"` to indicate the button opens a [modal](/docs/modals). T
|
|
|
115
115
|
<Button id="open-feedback" actionType="modal">Give Feedback</Button>
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
Optional `callbackUrl` causes the action data to be POSTed to a URL when clicked. See [Callback URLs](/docs/actions#callback-urls) for details.
|
|
119
|
+
|
|
120
|
+
```tsx title="lib/bot.tsx"
|
|
121
|
+
<Button callbackUrl={webhook.url} id="approve" style="primary">Approve</Button>
|
|
122
|
+
```
|
|
123
|
+
|
|
118
124
|
### CardLink
|
|
119
125
|
|
|
120
126
|
Inline hyperlink rendered as text. Unlike `LinkButton` (which must be inside `Actions`), `CardLink` can be placed directly in a card alongside other content.
|
package/docs/concurrency.mdx
CHANGED
|
@@ -124,6 +124,10 @@ const bot = new Chat({
|
|
|
124
124
|
| `debounceMs` | debounce | `1500` | Debounce window in milliseconds |
|
|
125
125
|
| `maxConcurrent` | concurrent | `Infinity` | Max concurrent handlers per thread |
|
|
126
126
|
|
|
127
|
+
<Callout type="warn">
|
|
128
|
+
`maxConcurrent` only applies to the `concurrent` strategy. Pairing it with any other strategy logs a warning and the value is ignored. Setting `maxConcurrent` to a value less than `1` throws at construction time — `0` would deadlock the strategy and is rejected up front.
|
|
129
|
+
</Callout>
|
|
130
|
+
|
|
127
131
|
## MessageContext
|
|
128
132
|
|
|
129
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.
|
|
@@ -414,6 +414,62 @@ async deleteMessage(threadId: string, messageId: string): Promise<void> {
|
|
|
414
414
|
}
|
|
415
415
|
```
|
|
416
416
|
|
|
417
|
+
### Buttons and callback URLs
|
|
418
|
+
|
|
419
|
+
When the host app passes a `Card` with `<Button callbackUrl={...}>`, the SDK rewrites each such button **before** your adapter sees the postable: the `callbackUrl` is stored in the state adapter under a short token, and the button's `value` field is replaced with `__cb:<16-hex-chars>` (21 characters total). Your adapter does not need to know this happens — just round-trip `button.value` through your platform's button payload.
|
|
420
|
+
|
|
421
|
+
What this means in practice:
|
|
422
|
+
|
|
423
|
+
1. **Send side**: when rendering a `ButtonElement` to your platform's button payload, encode both `button.id` (the action ID) and `button.value` (which may be `undefined`, a user-supplied value, or a callback token). Pick a delimiter that cannot appear in either, and validate the encoded string fits the platform's limit.
|
|
424
|
+
2. **Receive side**: when the user clicks a button, decode the platform payload back into `actionId` and `value`, and pass them to `chat.processAction({ actionId, value, ... })`. The SDK will detect the `__cb:` prefix, look up the stored callback URL, POST to it, and pass the original value (if any) to user `onAction` handlers.
|
|
425
|
+
|
|
426
|
+
Discord's adapter is a good reference — it joins the action ID and value with `\n` and validates against Discord's 100-character `custom_id` limit:
|
|
427
|
+
|
|
428
|
+
```typescript title="src/cards.ts (discord)" lineNumbers
|
|
429
|
+
const DISCORD_CUSTOM_ID_DELIMITER = "\n";
|
|
430
|
+
const DISCORD_CUSTOM_ID_MAX_LENGTH = 100;
|
|
431
|
+
|
|
432
|
+
export function encodeDiscordCustomId(
|
|
433
|
+
actionId: string,
|
|
434
|
+
value?: string
|
|
435
|
+
): string {
|
|
436
|
+
if (value == null || value === "") {
|
|
437
|
+
validateLength(actionId);
|
|
438
|
+
return actionId;
|
|
439
|
+
}
|
|
440
|
+
const encoded = `${actionId}${DISCORD_CUSTOM_ID_DELIMITER}${value}`;
|
|
441
|
+
validateLength(encoded);
|
|
442
|
+
return encoded;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function decodeDiscordCustomId(customId: string): {
|
|
446
|
+
actionId: string;
|
|
447
|
+
value: string | undefined;
|
|
448
|
+
} {
|
|
449
|
+
const idx = customId.indexOf(DISCORD_CUSTOM_ID_DELIMITER);
|
|
450
|
+
if (idx === -1) {
|
|
451
|
+
return { actionId: customId, value: undefined };
|
|
452
|
+
}
|
|
453
|
+
// Use the FIRST delimiter only — values may legitimately contain "\n".
|
|
454
|
+
return {
|
|
455
|
+
actionId: customId.slice(0, idx),
|
|
456
|
+
value: customId.slice(idx + 1),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
<Callout type="info">
|
|
462
|
+
Platform button-data limits are the main constraint to plan for. Discord's
|
|
463
|
+
`custom_id` is 100 chars; Telegram's `callback_data` is 64 bytes. The SDK's
|
|
464
|
+
callback token is fixed at 21 chars (`__cb:` + 16 hex), so the worst-case
|
|
465
|
+
payload your encoding must fit is `actionId + delimiter + 21`. If the
|
|
466
|
+
encoded string exceeds the platform limit, throw a `ValidationError` from
|
|
467
|
+
`@chat-adapter/shared` so the host app fails fast at post time rather than
|
|
468
|
+
silently truncating.
|
|
469
|
+
</Callout>
|
|
470
|
+
|
|
471
|
+
Modals with `callbackUrl` are handled entirely inside the SDK via stored modal context — your adapter does not need any special handling. Just call `chat.processModalSubmit(event, contextId, { waitUntil })` from your webhook handler and the SDK will POST to the modal's `callbackUrl` (using `waitUntil` if you provide it, so the response is not blocked).
|
|
472
|
+
|
|
417
473
|
### Reactions
|
|
418
474
|
|
|
419
475
|
Handle both `EmojiValue` objects and plain strings. `EmojiValue` has a `name` property and `toString()` method — there is no `unicode` field.
|
|
@@ -535,7 +591,7 @@ export class MatrixFormatConverter extends BaseFormatConverter {
|
|
|
535
591
|
}
|
|
536
592
|
```
|
|
537
593
|
|
|
538
|
-
For platforms with non-standard formatting
|
|
594
|
+
For platforms with non-standard formatting, implement custom parsing in `toAst()` and rendering in `fromAst()`. See the [Discord adapter](https://github.com/vercel/chat/blob/main/packages/adapter-discord/src/markdown.ts) for an example of handling platform-specific mention syntax.
|
|
539
595
|
|
|
540
596
|
## Optional methods
|
|
541
597
|
|
|
@@ -639,3 +695,19 @@ import {
|
|
|
639
695
|
cardToFallbackText, // Convert card to plain text
|
|
640
696
|
} from "@chat-adapter/shared";
|
|
641
697
|
```
|
|
698
|
+
|
|
699
|
+
### Token encryption
|
|
700
|
+
|
|
701
|
+
If your adapter persists OAuth tokens to a `StateAdapter`, encrypt them at rest with the shared AES-256-GCM helpers instead of rolling your own:
|
|
702
|
+
|
|
703
|
+
```typescript
|
|
704
|
+
import {
|
|
705
|
+
encryptToken, // Encrypt a string into an EncryptedTokenData envelope
|
|
706
|
+
decryptToken, // Decrypt an envelope back to the original string
|
|
707
|
+
decodeKey, // Decode a hex-64 or base64-44 32-byte key (throws on wrong length)
|
|
708
|
+
isEncryptedTokenData, // Type guard for tolerating legacy plaintext records
|
|
709
|
+
type EncryptedTokenData,
|
|
710
|
+
} from "@chat-adapter/shared";
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
Accept the key as an optional `encryptionKey` config field (auto-detected from a `*_ENCRYPTION_KEY` env var), encrypt on `setInstallation()`, decrypt on `getInstallation()`, and use `isEncryptedTokenData` to keep accepting plaintext records so operators can roll the key in without flushing existing installs. See `@chat-adapter/slack` and `@chat-adapter/linear` for reference implementations.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Conversation History
|
|
3
|
+
description: Persist messages per user across every platform — for LLM context, audit, or compliance.
|
|
4
|
+
type: guide
|
|
5
|
+
prerequisites:
|
|
6
|
+
- /docs/state
|
|
7
|
+
related:
|
|
8
|
+
- /docs/handling-events
|
|
9
|
+
- /docs/api/transcripts
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
Bots that hold context across a user's conversations need somewhere to store it. The platform's own message history won't do — a user might talk to your bot in Slack today and Discord tomorrow, and you want the same memory to follow them.
|
|
13
|
+
|
|
14
|
+
`bot.transcripts` keeps a per-user transcript in your state adapter, keyed by a stable identifier you choose (an email, an internal user ID, anything that's the same person no matter where they are).
|
|
15
|
+
|
|
16
|
+
## Setup
|
|
17
|
+
|
|
18
|
+
You opt in by setting two fields on `ChatConfig`:
|
|
19
|
+
|
|
20
|
+
```typescript title="lib/bot.ts" lineNumbers
|
|
21
|
+
import { Chat } from "chat";
|
|
22
|
+
import { createSlackAdapter } from "@chat-adapter/slack";
|
|
23
|
+
import { createDiscordAdapter } from "@chat-adapter/discord";
|
|
24
|
+
import { createRedisState } from "@chat-adapter/state-redis";
|
|
25
|
+
|
|
26
|
+
const bot = new Chat({
|
|
27
|
+
userName: "mybot",
|
|
28
|
+
adapters: {
|
|
29
|
+
slack: createSlackAdapter(),
|
|
30
|
+
discord: createDiscordAdapter(),
|
|
31
|
+
},
|
|
32
|
+
state: createRedisState({ url: process.env.REDIS_URL! }),
|
|
33
|
+
|
|
34
|
+
// Resolve the cross-platform identifier for an inbound message.
|
|
35
|
+
// Return null for messages you don't want to remember.
|
|
36
|
+
identity: ({ author }) => author.email ?? null,
|
|
37
|
+
|
|
38
|
+
// Storage tuning. retention is the list TTL, refreshed on every append.
|
|
39
|
+
transcripts: {
|
|
40
|
+
retention: "30d",
|
|
41
|
+
maxPerUser: 200,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`transcripts` and `identity` are paired — set one without the other and the constructor throws. This keeps the API loud rather than silently no-op'ing on every call.
|
|
47
|
+
|
|
48
|
+
## Building LLM context
|
|
49
|
+
|
|
50
|
+
The most common pattern: append the user's message, build a prompt from recent transcript entries, post the reply, append the reply too.
|
|
51
|
+
|
|
52
|
+
```typescript title="lib/bot.ts" lineNumbers
|
|
53
|
+
bot.onSubscribedMessage(async (thread, msg) => {
|
|
54
|
+
await bot.transcripts.append(thread, msg);
|
|
55
|
+
|
|
56
|
+
const recent = await bot.transcripts.list({
|
|
57
|
+
userKey: msg.userKey!,
|
|
58
|
+
limit: 20,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const reply = await generateReply(recent, msg);
|
|
62
|
+
await thread.post(reply);
|
|
63
|
+
|
|
64
|
+
await bot.transcripts.append(
|
|
65
|
+
thread,
|
|
66
|
+
{ role: "assistant", text: reply },
|
|
67
|
+
{ userKey: msg.userKey! }
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
A few things worth knowing:
|
|
73
|
+
|
|
74
|
+
- **`msg.userKey`** is set automatically from your `identity` resolver before your handler runs. If the resolver returned `null`, it stays `undefined` and the `append` call no-ops.
|
|
75
|
+
- **Bot replies are explicit.** The SDK doesn't auto-capture `thread.post()` output — you decide what gets remembered. That's important for retries, intermediate streaming chunks, and anything you don't want feeding back into the model later.
|
|
76
|
+
- **Order is chronological.** `list` returns oldest-first, ready to feed into a model. Set `limit` to keep prompts bounded.
|
|
77
|
+
|
|
78
|
+
## Identity resolution
|
|
79
|
+
|
|
80
|
+
`identity` runs once per inbound message during dispatch. The `author`, `message`, and `adapter` name are all available:
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
identity: async ({ adapter, author, message }) => {
|
|
84
|
+
// Look up by email when the platform exposes it
|
|
85
|
+
if (author.email) {
|
|
86
|
+
return author.email;
|
|
87
|
+
}
|
|
88
|
+
// Or map a platform user to an internal ID
|
|
89
|
+
return await lookupUser(adapter, author.userId);
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Return `null` when you can't resolve a key. The SDK won't fall back to a platform-specific ID — that would silently fragment a user's transcript across platforms, which is exactly what this feature is here to prevent.
|
|
94
|
+
|
|
95
|
+
If your resolver throws, the SDK logs a warning and dispatches the message without a `userKey`. Handlers still run; only the persistence is skipped.
|
|
96
|
+
|
|
97
|
+
## Filtering entries
|
|
98
|
+
|
|
99
|
+
`list` accepts a few filters. They compose, and they're applied after `getList` — useful for narrowing prompts without restructuring storage.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// Recent N across all platforms
|
|
103
|
+
await bot.transcripts.list({ userKey: "mike@acme.com", limit: 50 });
|
|
104
|
+
|
|
105
|
+
// Single platform
|
|
106
|
+
await bot.transcripts.list({ userKey: "mike@acme.com", platforms: ["slack"] });
|
|
107
|
+
|
|
108
|
+
// Single thread
|
|
109
|
+
await bot.transcripts.list({
|
|
110
|
+
userKey: "mike@acme.com",
|
|
111
|
+
threadId: "slack:C123:1234.5678",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Only the user's own messages
|
|
115
|
+
await bot.transcripts.list({ userKey: "mike@acme.com", roles: ["user"] });
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Deleting a user's transcript
|
|
119
|
+
|
|
120
|
+
For data-subject requests or simple "forget me" flows:
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
await bot.transcripts.delete({ userKey: "mike@acme.com" });
|
|
124
|
+
// → { deleted: 47 }
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
This wipes every entry stored under the key. Single-entry and time-range deletes aren't part of the API — `appendToList` doesn't support them safely under concurrent writes.
|
|
128
|
+
|
|
129
|
+
## Where it's stored
|
|
130
|
+
|
|
131
|
+
`bot.transcripts` is backed by `StateAdapter.appendToList` / `getList` / `delete`. Every built-in state adapter (`memory`, `redis`, `ioredis`, `pg`) supports these primitives, so this works on whichever one you've already configured.
|
|
132
|
+
|
|
133
|
+
Entries are written under the key `transcripts:user:{userKey}` as a capped list. `appendToList` is atomic, so concurrent inbound messages on the same user don't race.
|
|
134
|
+
|
|
135
|
+
## Reference
|
|
136
|
+
|
|
137
|
+
See [Transcripts](/docs/api/transcripts) for full type signatures, configuration options, and the entry shape.
|
package/docs/direct-messages.mdx
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Direct
|
|
2
|
+
title: Direct Messages
|
|
3
3
|
description: Initiate DM conversations with users programmatically.
|
|
4
4
|
type: guide
|
|
5
5
|
prerequisites:
|
|
6
6
|
- /docs/usage
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
Open direct message conversations with users using `bot.openDM()`.
|
|
9
|
+
Open direct message conversations with users using `bot.openDM()`. For globally recognizable user IDs, the adapter is automatically inferred from the ID format.
|
|
10
10
|
|
|
11
11
|
## DM behavior
|
|
12
12
|
|
|
@@ -40,10 +40,19 @@ const dmThread = await bot.openDM("U1234567890"); // Slack
|
|
|
40
40
|
|
|
41
41
|
| Format | Platform |
|
|
42
42
|
|--------|----------|
|
|
43
|
-
| `U...` | Slack |
|
|
43
|
+
| `U...` / `W...` | Slack |
|
|
44
44
|
| `29:...` | Teams |
|
|
45
45
|
| `users/...` | Google Chat |
|
|
46
|
-
|
|
|
46
|
+
| Numeric ID | Discord or Telegram |
|
|
47
|
+
|
|
48
|
+
<Callout type="info">
|
|
49
|
+
Numeric IDs can be ambiguous when multiple numeric-ID adapters are registered. For platforms whose user IDs are not globally distinguishable, call the adapter directly and wrap the returned thread ID with `bot.thread()`.
|
|
50
|
+
</Callout>
|
|
51
|
+
|
|
52
|
+
```typescript title="lib/bot.ts"
|
|
53
|
+
const threadId = await bot.getAdapter("whatsapp").openDM("15551234567");
|
|
54
|
+
const dmThread = bot.thread(threadId);
|
|
55
|
+
```
|
|
47
56
|
|
|
48
57
|
## Check if a thread is a DM
|
|
49
58
|
|
package/docs/error-handling.mdx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
title: Error
|
|
2
|
+
title: Error Handling
|
|
3
3
|
description: Handle rate limits, unsupported features, and other errors from adapters.
|
|
4
4
|
type: guide
|
|
5
5
|
prerequisites:
|
|
@@ -16,7 +16,19 @@ import { ChatError, RateLimitError, NotImplementedError, LockError } from "chat"
|
|
|
16
16
|
|
|
17
17
|
### ChatError
|
|
18
18
|
|
|
19
|
-
Base error class for all SDK errors. Every error below extends `ChatError`.
|
|
19
|
+
Base error class for all SDK errors. Every error below extends `ChatError`. The `code` property carries a machine-readable identifier you can branch on:
|
|
20
|
+
|
|
21
|
+
| Code | Thrown by | Meaning |
|
|
22
|
+
|------|-----------|---------|
|
|
23
|
+
| `NOT_SUPPORTED` | `bot.openDM`, `bot.getUser` | The resolved adapter doesn't implement this method |
|
|
24
|
+
| `INVALID_THREAD_ID` | `bot.thread`, internal routing | Thread ID does not match the `adapter:channel:thread` shape |
|
|
25
|
+
| `INVALID_CHANNEL_ID` | `bot.channel` | Channel ID does not match the `adapter:channel` shape |
|
|
26
|
+
| `ADAPTER_NOT_FOUND` | `bot.thread`, `bot.channel` | Thread/channel ID references an adapter that wasn't registered on this `Chat` instance |
|
|
27
|
+
| `AMBIGUOUS_USER_ID` | `bot.getUser`, `bot.openDM` | Numeric user ID could match more than one registered adapter (Discord/Telegram/GitHub) |
|
|
28
|
+
| `UNKNOWN_USER_ID_FORMAT` | `bot.getUser`, `bot.openDM` | The `userId` doesn't match any platform's known ID format |
|
|
29
|
+
| `RATE_LIMITED` | Any platform call | Platform returned 429; see `RateLimitError` below |
|
|
30
|
+
| `NOT_IMPLEMENTED` | Any platform call | The adapter doesn't implement this feature; see `NotImplementedError` below |
|
|
31
|
+
| `LOCK_FAILED` | Inbound message routing | Distributed lock was busy; see `LockError` below |
|
|
20
32
|
|
|
21
33
|
<TypeTable
|
|
22
34
|
type={{
|
|
@@ -25,7 +37,7 @@ Base error class for all SDK errors. Every error below extends `ChatError`.
|
|
|
25
37
|
type: 'string',
|
|
26
38
|
},
|
|
27
39
|
code: {
|
|
28
|
-
description: 'Machine-readable error code.',
|
|
40
|
+
description: 'Machine-readable error code (see table above).',
|
|
29
41
|
type: 'string',
|
|
30
42
|
},
|
|
31
43
|
cause: {
|
package/docs/files.mdx
CHANGED