Skip to content

Commit 3abdc69

Browse files
authored
docs(adapters): add Cloudflare Agents as vendor-official state adapter (#669)
Adds Cloudflare Agents as a vendor-official **state** adapter — `agents/chat-sdk`'s `createChatSdkState()`, a Chat SDK `StateAdapter` that stores subscriptions, locks, queues, dedupe keys, thread/channel state, transcripts, and history in Durable Object SQLite via `ChatSdkStateAgent` sub-agents. - `vendor-official/cloudflare-agents.mdx` state-adapter page (Agent setup, wrangler DO migration, sharding, config, storage/cleanup) - catalog entry in `packages/chat/src/adapters/index.ts` (`group: vendor-official`, `type: state`) - registry entry in `adapters.json` + `cloudflare-agents` in vendor-official `meta.json` - integration-test doc lists + changeset Repo: https://github.com/cloudflare/agents · package `agents` (`agents/chat-sdk`) · [docs](https://developers.cloudflare.com/agents/runtime/communication/chat-sdk/) ### Not wired into the create-chat-sdk CLI This adapter runs inside a Cloudflare Worker with Durable Objects, not the generated Next.js runtime, so it is intentionally kept out of the scaffold: - added to `CLI_INCOMPATIBLE_ADAPTERS` (rejected via `--adapter`, hidden from the platform picker and e2e run, like `lark`/`matrix`) - new `listCliStateAdapters()` filters the interactive **state** picker and the `--help` adapter list (the state picker previously used raw `listStateAdapters()` and would have offered it, then thrown on selection) ### Tests - `catalog/display.test.ts` — `listCliStateAdapters`: returns only state adapters, includes `memory`/`redis`, and excludes `cloudflare-agents` while asserting it *is* in the raw catalog - `catalog/selection.test.ts` — `resolveAdapterValue("cloudflare-agents")` throws "not supported" - `cli/program.test.ts` — `buildAdapterList()` help text omits `cloudflare-agents` - existing `CLI_SCAFFOLD_SPEC covers every catalog adapter` + docs-adapters/docs-content suites cover the catalog entry, registry parity, and MDX imports ### Validation - create-chat-sdk: **178 passed**, typecheck clean - integration docs suites pass; Biome + knip clean --------- Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
1 parent 7bca40b commit 3abdc69

18 files changed

Lines changed: 309 additions & 12 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"chat": patch
3+
"create-chat-sdk": patch
4+
---
5+
6+
docs(adapters): add Cloudflare Agents as a vendor-official state adapter (`agents/chat-sdk`) to the catalog and docs listing. It is hidden from the create-chat-sdk CLI (Worker/Durable Objects runtime), and the interactive state picker now filters out CLI-incompatible state adapters.

apps/docs/adapters.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,17 @@
159159
"author": "dcartertwo",
160160
"readme": "https://github.com/dcartertwo/chat-state-cloudflare-do/tree/13ebf545b302b7c8fdabf7de9c5de678e047ce5e"
161161
},
162+
{
163+
"name": "Cloudflare Agents",
164+
"slug": "cloudflare-agents",
165+
"type": "state",
166+
"community": true,
167+
"description": "Cloudflare Agents state adapter for Chat SDK. Stores subscriptions, locks, queues, and history in Durable Object SQLite via ChatSdkStateAgent sub-agents.",
168+
"packageName": "agents",
169+
"author": "Cloudflare",
170+
"readme": "https://github.com/cloudflare/agents",
171+
"vendorOfficial": true
172+
},
162173
{
163174
"name": "MySQL",
164175
"slug": "mysql",
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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 />

apps/docs/content/adapters/vendor-official/meta.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"velt",
1313
"kapso",
1414
"linq",
15-
"photon"
15+
"photon",
16+
"cloudflare-agents"
1617
]
1718
}

packages/chat/src/adapters/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,22 @@ export const ADAPTERS = {
198198
slug: "agentphone",
199199
type: "platform",
200200
},
201+
"cloudflare-agents": {
202+
description:
203+
"Cloudflare Agents state adapter for Chat SDK. Stores subscriptions, locks, queues, and history in Durable Object SQLite via ChatSdkStateAgent sub-agents.",
204+
env: {
205+
config: ["parent", "agent", "name", "shardKey", "keyShard"],
206+
notes:
207+
"No environment variables are required. State is stored in Durable Object SQLite via ChatSdkStateAgent sub-agents; add the parent Agent to your Durable Object migration and re-export ChatSdkStateAgent from the Worker entry point.",
208+
},
209+
factoryExport: "createChatSdkState",
210+
group: "vendor-official",
211+
name: "Cloudflare Agents",
212+
packageName: "agents",
213+
peerDeps: [],
214+
slug: "cloudflare-agents",
215+
type: "state",
216+
},
201217
discord: {
202218
description:
203219
"Create Discord bots with slash commands, threads, and rich embeds.",

packages/create-chat-sdk/src/catalog/compatibility.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import type { AdapterSlug } from "chat/adapters";
1010
* are hidden from the CLI prompts and rejected when passed via `--adapter`.
1111
*/
1212
const CLI_INCOMPATIBLE_ADAPTERS = {
13+
"cloudflare-agents":
14+
"it runs inside a Cloudflare Worker with Durable Objects, not the generated Next.js runtime",
1315
lark: "it requires a long-running WebSocket connection",
1416
matrix: "it requires a long-running sync process",
1517
} as const satisfies Partial<Record<AdapterSlug, string>>;

packages/create-chat-sdk/src/catalog/display.test.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { listPlatformAdapters } from "chat/adapters";
1+
import { listPlatformAdapters, listStateAdapters } from "chat/adapters";
22
import { describe, expect, it } from "vitest";
3-
import { listCliPlatformAdapters } from "./display.js";
3+
import { listCliPlatformAdapters, listCliStateAdapters } from "./display.js";
44

55
describe("listCliPlatformAdapters", () => {
66
it("keeps official adapters in catalog order", () => {
@@ -27,3 +27,26 @@ describe("listCliPlatformAdapters", () => {
2727
expect(slugs).not.toContain("lark");
2828
});
2929
});
30+
31+
describe("listCliStateAdapters", () => {
32+
it("returns only state adapters", () => {
33+
expect(
34+
listCliStateAdapters().every((adapter) => adapter.type === "state")
35+
).toBe(true);
36+
});
37+
38+
it("includes state adapters the scaffold can host", () => {
39+
const slugs = listCliStateAdapters().map((adapter) => adapter.slug);
40+
expect(slugs).toContain("memory");
41+
expect(slugs).toContain("redis");
42+
});
43+
44+
it("omits state adapters the webhook-only scaffold cannot host", () => {
45+
const catalogSlugs = listStateAdapters().map((adapter) => adapter.slug);
46+
const cliSlugs = listCliStateAdapters().map((adapter) => adapter.slug);
47+
// Cloudflare Agents ships in the catalog but runs inside a Worker, so it is
48+
// hidden from the CLI even though it is a catalog state adapter.
49+
expect(catalogSlugs).toContain("cloudflare-agents");
50+
expect(cliSlugs).not.toContain("cloudflare-agents");
51+
});
52+
});

packages/create-chat-sdk/src/catalog/display.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { CatalogAdapter } from "chat/adapters";
2-
import { listPlatformAdapters } from "chat/adapters";
2+
import { listPlatformAdapters, listStateAdapters } from "chat/adapters";
33
import { isCliCompatibleAdapter } from "./compatibility.js";
44

55
type PlatformAdapterGroup = CatalogAdapter["group"];
@@ -31,3 +31,18 @@ export function listCliPlatformAdapters(
3131

3232
return adapters;
3333
}
34+
35+
/**
36+
* List state adapters the CLI can scaffold.
37+
*
38+
* Filters out catalog state adapters the generated Next.js runtime cannot host
39+
* (for example, Cloudflare Agents, which runs inside a Worker with Durable
40+
* Objects) so they never appear in the interactive state picker.
41+
*
42+
* @returns Scaffoldable state adapters in catalog order.
43+
*/
44+
export function listCliStateAdapters(): CatalogAdapter[] {
45+
return listStateAdapters().filter((adapter) =>
46+
isCliCompatibleAdapter(adapter.slug)
47+
);
48+
}

packages/create-chat-sdk/src/catalog/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { listCliPlatformAdapters } from "./display.js";
1+
export { listCliPlatformAdapters, listCliStateAdapters } from "./display.js";
22
export {
33
type AdapterConnectSpec,
44
CONNECT_CHAT_IMPORT,

packages/create-chat-sdk/src/catalog/scaffold-spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ export const CLI_SCAFFOLD_SPEC = {
130130
agentphone: {
131131
invocation: { kind: "empty-object" },
132132
},
133+
// Catalog-only: hidden from the CLI via CLI_INCOMPATIBLE_ADAPTERS. Runs inside
134+
// a Cloudflare Worker, so it is never scaffolded by create-chat-sdk.
135+
"cloudflare-agents": {
136+
invocation: { kind: "zero-arg" },
137+
},
133138
discord: {
134139
invocation: { kind: "zero-arg" },
135140
serverExternalPackages: [

0 commit comments

Comments
 (0)