Chat Agent
Durable AI chat agent with SQLite-backed Durable Objects and optional Workers AI
Minimal durable AI chat agent using a SQLite-backed Durable Object. Workers AI is optional - without an AI binding the agent echoes agent: <message>.
Features
- Per-session chat history in Durable Object SQLite
POST /chatfor request/response turnsGET /chatfor history- WebSocket upgrade on
GET /wsfor realtime updates - Optional Workers AI replies when the
AIbinding is available
API Reference
POST /chat
Send a user message and receive the updated message list (including the assistant reply).
sessionId string (required)
Session id (letters, numbers, _, -).
message string (required)
User message (max 4,000 characters).
Example Request
curl -X POST "https://your-worker.workers.dev/chat" \
-H "Content-Type: application/json" \
-d '{"sessionId":"demo","message":"Hello"}'Success Response
{
"sessionId": "demo",
"messages": [
{ "role": "user", "content": "Hello" },
{ "role": "assistant", "content": "agent: Hello" }
]
}GET /chat
Return chat history for a session.
sessionId string (required)
Example Request
curl "https://your-worker.workers.dev/chat?sessionId=demo"GET /ws
WebSocket upgrade for realtime message updates. Send { "message": "..." } over the socket; receive { "type": "messages", "messages": [...] }.
sessionId string (required)
Requires Upgrade: websocket.
Error Codes
400- Invalid session, message, body, or missing WebSocket upgrade (INVALID_SESSION,INVALID_MESSAGE,INVALID_BODY,EXPECTED_WEBSOCKET)502- Chat or Durable Object failure (CHAT_ERROR)
Use Cases
- Learn Agents / Durable Object chat session patterns
- Prototype multi-turn AI assistants with persistent history
- Add WebSocket streaming alongside HTTP chat APIs
- Compare stub echo replies vs Workers AI generation
Limitations
- Without Workers AI, replies use the stub format
agent: <message> - Message max 4,000 characters; no auth on sessions
- Demo is single-agent per session id, not multi-user rooms
- WebSocket clients must send JSON
{ "message": "..." }
Use in your project
Copy these files into an existing Worker. Prefer Deployment to try the full experiment first. Source: apps/experiments/chat-agent.
import { AI_MODEL, MAX_MESSAGE_LENGTH, SESSION_ID_PATTERN } from "../constants/defaults";import type { ChatHistoryResponse, ChatMessage, ChatPostResponse } from "../types/chat";import type { Env } from "../types/env";export function validateSessionId(input: string | undefined): string | null { if (!input || typeof input !== "string") return null; const trimmed = input.trim(); if (!SESSION_ID_PATTERN.test(trimmed)) return null; return trimmed;}export function validateMessage(input: string | undefined): string | null { if (typeof input !== "string") return null; const trimmed = input.trim(); if (!trimmed || trimmed.length > MAX_MESSAGE_LENGTH) return null; return trimmed;}export function stubAgentReply(message: string): string { return `agent: ${message}`;}type AiTextResponse = { response?: string;};export async function generateReply(ai: Ai | undefined, message: string): Promise<string> { if (!ai) { return stubAgentReply(message); } try { const result = (await ai.run(AI_MODEL, { messages: [{ role: "user", content: message }], })) as AiTextResponse; const response = result.response?.trim(); if (response) return response; } catch { // Fall through to stub reply when AI is unavailable. } return stubAgentReply(message);}export function getChatStub(env: Env, sessionId: string): DurableObjectStub { const id = env.CHAT_AGENT.idFromName(sessionId); return env.CHAT_AGENT.get(id);}export async function postChatMessage( env: Env, sessionId: string, message: string): Promise<ChatPostResponse> { const stub = getChatStub(env, sessionId); const response = await stub.fetch("https://chat-agent/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message }), }); if (!response.ok) { throw new Error(`Chat post failed with status ${response.status}`); } return (await response.json()) as ChatPostResponse;}export async function getChatHistory(env: Env, sessionId: string): Promise<ChatHistoryResponse> { const stub = getChatStub(env, sessionId); const response = await stub.fetch("https://chat-agent/chat"); if (!response.ok) { throw new Error(`Chat history failed with status ${response.status}`); } return (await response.json()) as ChatHistoryResponse;}export async function upgradeChatWebSocket( env: Env, sessionId: string, request: Request): Promise<Response> { const stub = getChatStub(env, sessionId); return stub.fetch("https://chat-agent/ws", request);}export function mapSqlRowsToMessages( rows: Array<{ id: number; role: string; content: string; created_at: string }>): ChatMessage[] { return rows.map((row) => ({ id: row.id, role: row.role === "assistant" ? "assistant" : "user", content: row.content, createdAt: row.created_at, }));}Deployment
Deploy
Wrangler binds CHAT_AGENT to class ChatAgent (SQLite migration v1) and AI for Workers AI.
Test your deployment
curl -X POST "https://your-worker.workers.dev/chat" \
-H "Content-Type: application/json" \
-d '{"sessionId":"demo","message":"Hello"}'Local Development
cd apps/experiments/chat-agent
npm install
npm run devcurl -X POST "http://localhost:8787/chat" \
-H "Content-Type: application/json" \
-d '{"sessionId":"demo","message":"Hello"}'AI replies require a Workers AI binding. Without it, replies use the stub format.
Configuration
wrangler.json declares:
- Durable Object binding
CHAT_AGENT→ChatAgent - Migration
v1withnew_sqlite_classes: ["ChatAgent"] - Workers AI binding
AI
Cloudflare Features Used
- Workers - Edge compute runtime
- Durable Objects - SQLite-backed chat sessions
- Workers AI - Optional model replies
- WebSockets - Realtime
/wsupgrades