|
| 1 | +--- |
| 2 | +title: Cloudflare Agents |
| 3 | +description: Vendor-official state adapter for Chat SDK backed by Cloudflare Agents. Stores subscriptions, locks, queues, dedupe keys, thread and channel state, transcripts, and message history in Durable Object SQLite via ChatSdkStateAgent sub-agents. |
| 4 | +packageName: agents |
| 5 | +slug: cloudflare-agents |
| 6 | +type: state |
| 7 | +tagline: State adapter for Chat SDK that persists subscriptions, locks, queues, and history in Durable Object SQLite — sharded across Cloudflare Agents sub-agents, with no Redis or external database. |
| 8 | +community: true |
| 9 | +vendorOfficial: true |
| 10 | +author: Cloudflare |
| 11 | +mdxBody: true |
| 12 | +features: |
| 13 | + persistence: yes |
| 14 | + multiInstance: yes |
| 15 | + subscriptions: yes |
| 16 | + distributedLocking: yes |
| 17 | + keyValueCache: yes |
| 18 | + lists: yes |
| 19 | + queues: yes |
| 20 | + automaticReconnect: yes |
| 21 | + cluster: |
| 22 | + status: partial |
| 23 | + label: Sharding |
| 24 | + sentinel: no |
| 25 | + keyPrefix: |
| 26 | + status: yes |
| 27 | + label: shardKey |
| 28 | +--- |
| 29 | + |
| 30 | +Use `agents/chat-sdk` when you run [Chat SDK](https://chat-sdk.dev) inside a [Cloudflare Agent](https://developers.cloudflare.com/agents/). It provides a Chat SDK `StateAdapter` that stores subscriptions, locks, queues, dedupe keys, thread and channel state, callback metadata, transcripts, and thread history in Durable Object SQLite. Each state shard is a `ChatSdkStateAgent` sub-agent under your ingress Agent. |
| 31 | + |
| 32 | +Use it with any Chat SDK adapter — Telegram, Slack, Discord, Teams, or Google Chat. |
| 33 | + |
| 34 | +## Install |
| 35 | + |
| 36 | +<PackageInstall package="agents chat" /> |
| 37 | + |
| 38 | +## Quick start |
| 39 | + |
| 40 | +Create a parent Agent that owns your Chat SDK runtime and pass `createChatSdkState()` as the `state` option. Export `ChatSdkStateAgent` from your Worker entry point so sub-agent routing can resolve it. |
| 41 | + |
| 42 | +```typescript title="src/index.ts" lineNumbers |
| 43 | +import { Agent } from "agents"; |
| 44 | +import { createChatSdkState } from "agents/chat-sdk"; |
| 45 | +import { Chat } from "chat"; |
| 46 | +import { createTelegramAdapter } from "@chat-adapter/telegram"; |
| 47 | + |
| 48 | +export { ChatSdkStateAgent } from "agents/chat-sdk"; |
| 49 | + |
| 50 | +export class MessengerAgent extends Agent<Env> { |
| 51 | + private chat!: Chat; |
| 52 | + |
| 53 | + onStart() { |
| 54 | + const telegram = createTelegramAdapter({ |
| 55 | + botToken: this.env.TELEGRAM_BOT_TOKEN, |
| 56 | + mode: "webhook", |
| 57 | + userName: "my_bot", |
| 58 | + }); |
| 59 | + |
| 60 | + this.chat = new Chat({ |
| 61 | + adapters: { telegram }, |
| 62 | + userName: "my_bot", |
| 63 | + state: createChatSdkState(), |
| 64 | + concurrency: { strategy: "burst", debounceMs: 600 }, |
| 65 | + }); |
| 66 | + } |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +When `createChatSdkState()` runs inside an Agent lifecycle method or request handler, it uses the current Agent as the parent (via `getCurrentAgent()`) and creates state shards with `this.subAgent()`. |
| 71 | + |
| 72 | +## Wrangler configuration |
| 73 | + |
| 74 | +Add the parent Agent to your Durable Object migration and enable `nodejs_compat`: |
| 75 | + |
| 76 | +```jsonc title="wrangler.jsonc" |
| 77 | +{ |
| 78 | + "compatibility_date": "2026-07-02", |
| 79 | + "compatibility_flags": ["nodejs_compat"], |
| 80 | + "durable_objects": { |
| 81 | + "bindings": [{ "class_name": "MessengerAgent", "name": "MessengerAgent" }] |
| 82 | + }, |
| 83 | + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MessengerAgent"] }] |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +```toml title="wrangler.toml" |
| 88 | +compatibility_date = "2026-07-02" |
| 89 | +compatibility_flags = ["nodejs_compat"] |
| 90 | + |
| 91 | +[[durable_objects.bindings]] |
| 92 | +class_name = "MessengerAgent" |
| 93 | +name = "MessengerAgent" |
| 94 | + |
| 95 | +[[migrations]] |
| 96 | +new_sqlite_classes = ["MessengerAgent"] |
| 97 | +tag = "v1" |
| 98 | +``` |
| 99 | + |
| 100 | +## State sharding |
| 101 | + |
| 102 | +By default, state is sharded by the first two colon-separated segments of a thread-like key. For example, `telegram:-100123:456` and `telegram:-100123:789` share the shard `telegram:-100123`. |
| 103 | + |
| 104 | +The default key sharder recognizes these Chat SDK key prefixes: |
| 105 | + |
| 106 | +- `thread-state:` |
| 107 | +- `channel-state:` |
| 108 | +- `msg-history:` |
| 109 | +- `transcripts:user:` |
| 110 | + |
| 111 | +Unknown keys use the adapter's default shard name, `default`. |
| 112 | + |
| 113 | +### Custom sharding |
| 114 | + |
| 115 | +Use `shardKey` to control how thread IDs map to state sub-agent names, and `keyShard` for non-thread-shaped keys that should still route to a provider-specific shard: |
| 116 | + |
| 117 | +```typescript |
| 118 | +const state = createChatSdkState({ |
| 119 | + shardKey(threadId) { |
| 120 | + return threadId.split(":").slice(0, 2).join(":"); |
| 121 | + }, |
| 122 | + keyShard(key) { |
| 123 | + if (!key.startsWith("dedupe:telegram:")) { |
| 124 | + return undefined; |
| 125 | + } |
| 126 | + |
| 127 | + const chatId = key.slice("dedupe:telegram:".length).split(":")[0]; |
| 128 | + return chatId ? `telegram:${chatId}` : undefined; |
| 129 | + }, |
| 130 | +}); |
| 131 | +``` |
| 132 | + |
| 133 | +Returning `undefined` from `keyShard` falls back to the built-in key sharder and then to the default shard. |
| 134 | + |
| 135 | +## Configuration |
| 136 | + |
| 137 | +<TypeTable |
| 138 | + type={{ |
| 139 | + parent: { |
| 140 | + type: "Agent", |
| 141 | + description: |
| 142 | + "Parent Agent that calls `subAgent()` to create state shards. Defaults to the current Agent from `getCurrentAgent()`.", |
| 143 | + }, |
| 144 | + agent: { |
| 145 | + type: "typeof ChatSdkStateAgent", |
| 146 | + description: |
| 147 | + "Custom subclass of `ChatSdkStateAgent`. Defaults to `ChatSdkStateAgent`.", |
| 148 | + }, |
| 149 | + name: { |
| 150 | + type: "string", |
| 151 | + default: '"default"', |
| 152 | + description: "Default shard name for keys that cannot be mapped.", |
| 153 | + }, |
| 154 | + shardKey: { |
| 155 | + type: "(threadId: string) => string", |
| 156 | + description: "Maps Chat SDK thread IDs and lock keys to a shard name.", |
| 157 | + }, |
| 158 | + keyShard: { |
| 159 | + type: "(key: string) => string | undefined", |
| 160 | + description: |
| 161 | + "Maps generic Chat SDK cache or list keys to a shard name. Return `undefined` to fall back to the built-in sharder.", |
| 162 | + }, |
| 163 | + }} |
| 164 | +/> |
| 165 | + |
| 166 | +## What is stored |
| 167 | + |
| 168 | +The adapter implements the full Chat SDK `StateAdapter` interface: |
| 169 | + |
| 170 | +- Subscriptions for `thread.subscribe()` and `thread.unsubscribe()`. |
| 171 | +- Locks for per-thread or per-channel concurrency. |
| 172 | +- Pending message queues for `queue`, `debounce`, and `burst` concurrency strategies. |
| 173 | +- Generic key-value cache entries with optional TTL. |
| 174 | +- Append-only lists with max-length trimming and list-level TTL refresh. |
| 175 | + |
| 176 | +Chat SDK features built on these primitives include message deduplication, thread and channel state, persistent thread history (for adapters that opt in to `persistThreadHistory`), callback URL token storage, modal context storage, and cross-platform transcripts. |
| 177 | + |
| 178 | +## Cleanup behavior |
| 179 | + |
| 180 | +TTL reads are strict: expired locks, cache values, queue entries, and list entries are ignored or deleted before they are returned. |
| 181 | + |
| 182 | +Physical cleanup is lazy. `ChatSdkStateAgent` schedules one cleanup callback for the earliest known expiry and reschedules after cleanup runs. This keeps idle shards quiet while preventing expired rows from accumulating indefinitely. |
| 183 | + |
| 184 | +## API |
| 185 | + |
| 186 | +### `createChatSdkState(options)` |
| 187 | + |
| 188 | +Creates a Chat SDK `StateAdapter` backed by a `ChatSdkStateAgent` sub-agent. Options are optional — the defaults resolve the parent from `getCurrentAgent()` and shard by thread-like key prefixes. |
| 189 | + |
| 190 | +### `ChatSdkStateAgent` |
| 191 | + |
| 192 | +The sub-agent class that stores state in SQLite. Export it from your Worker entry point so the runtime can create it: |
| 193 | + |
| 194 | +```typescript |
| 195 | +export { ChatSdkStateAgent } from "agents/chat-sdk"; |
| 196 | +``` |
| 197 | + |
| 198 | +## Example |
| 199 | + |
| 200 | +- [GitHub](https://github.com/cloudflare/agents) |
| 201 | +- [Chat SDK messenger example](https://github.com/cloudflare/agents/tree/main/examples/chat-sdk-messenger) — a Telegram messenger bot with Chat SDK state in sub-agents, burst/debounce concurrency, and AI replies running in managed fibers. |
| 202 | +- [Cloudflare Agents docs](https://developers.cloudflare.com/agents/runtime/communication/chat-sdk/) |
| 203 | + |
| 204 | +## Feature support |
| 205 | + |
| 206 | +<FeatureSupport /> |
0 commit comments