Cloudflare Agents

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.

Vendor-official adapter maintained by Cloudflare, not Vercel or Chat SDK contributors. For feature requests, bug reports, and support, file an issue on the adapter's repo.

Install

pnpm add agents chat

Quick start

Import the adapter from agents/chat-sdk when you run Chat SDK inside a Cloudflare Agent. Each state shard is a ChatSdkStateAgent sub-agent under your ingress Agent, and the adapter works with any Chat SDK platform adapter, including Telegram, Slack, Discord, Teams, and Google Chat.

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.

src/index.ts
import { Agent } from "agents";
import { createChatSdkState } from "agents/chat-sdk";
import { Chat } from "chat";
import { createTelegramAdapter } from "@chat-adapter/telegram";

export { ChatSdkStateAgent } from "agents/chat-sdk";

export class MessengerAgent extends Agent<Env> {
  private chat!: Chat;

  onStart() {
    const telegram = createTelegramAdapter({
      botToken: this.env.TELEGRAM_BOT_TOKEN,
      mode: "webhook",
      userName: "my_bot",
    });

    this.chat = new Chat({
      adapters: { telegram },
      userName: "my_bot",
      state: createChatSdkState(),
      concurrency: { strategy: "burst", debounceMs: 600 },
    });
  }
}

When createChatSdkState() runs inside an Agent lifecycle method or request handler, it gets the current Agent from getCurrentAgent(), uses it as the parent, and creates state shards with this.subAgent().

Configuration

Pass these options to createChatSdkState(). All of them are optional:

Prop

Type

Wrangler configuration

Add the parent Agent to your Durable Object migration and enable nodejs_compat:

wrangler.jsonc
{
  "compatibility_date": "2026-07-02",
  "compatibility_flags": ["nodejs_compat"],
  "durable_objects": {
    "bindings": [{ "class_name": "MessengerAgent", "name": "MessengerAgent" }]
  },
  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MessengerAgent"] }]
}
wrangler.toml
compatibility_date = "2026-07-02"
compatibility_flags = ["nodejs_compat"]

[[durable_objects.bindings]]
class_name = "MessengerAgent"
name = "MessengerAgent"

[[migrations]]
new_sqlite_classes = ["MessengerAgent"]
tag = "v1"

Data model

The adapter implements the full Chat SDK StateAdapter interface:

  • Subscriptions for thread.subscribe() and thread.unsubscribe().
  • Locks for per-thread or per-channel concurrency.
  • Pending message queues for queue, debounce, and burst concurrency strategies.
  • Generic key-value cache entries with optional TTL.
  • Append-only lists with max-length trimming and list-level TTL refresh.

Chat SDK builds message deduplication, thread and channel state, callback URL token storage, modal context storage, and cross-platform transcripts on these primitives. Persistent thread history uses them too, for adapters that opt in to persistThreadHistory.

State sharding

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.

The default key sharder recognizes these Chat SDK key prefixes:

  • thread-state:
  • channel-state:
  • msg-history:
  • transcripts:user:

Unknown keys use the adapter's default shard name, default.

Telegram Business threads (telegram:biz:{connectionId}:{chatId}) all share the shard telegram:biz under this rule. Use a custom shardKey that keeps the connection ID if you expect many business conversations.

Custom sharding

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:

const state = createChatSdkState({
  shardKey(threadId) {
    return threadId.split(":").slice(0, 2).join(":");
  },
  keyShard(key) {
    if (!key.startsWith("dedupe:telegram:")) {
      return undefined;
    }

    const chatId = key.slice("dedupe:telegram:".length).split(":")[0];
    return chatId ? `telegram:${chatId}` : undefined;
  },
});

Returning undefined from keyShard falls back to the built-in key sharder and then to the default shard.

Cleanup behavior

TTL reads are strict: expired locks, cache values, queue entries, and list entries are ignored or deleted before they are returned.

Physical cleanup is lazy. ChatSdkStateAgent schedules one cleanup callback for the earliest known expiry and reschedules after cleanup runs, so idle shards stay quiet and expired rows don't accumulate.

API

createChatSdkState(options)

Creates a Chat SDK StateAdapter backed by a ChatSdkStateAgent sub-agent. With no options, it resolves the parent from getCurrentAgent() and shards by thread-like key prefixes. See Configuration for the full option list.

ChatSdkStateAgent

The sub-agent class that stores state in SQLite. Export it from your Worker entry point so the runtime can create it:

export { ChatSdkStateAgent } from "agents/chat-sdk";

Feature support

Capabilities

FeatureSupported
Persistence
Multi-instance
Subscriptions
Distributed locking
Key-value caching
Lists
Queues
Automatic reconnect
Cluster supportSharding
Sentinel support
Key prefix namespacingshardKey

Resources