How SAM Agents Use Durable Objects for Stateful Chat Interactions
SAM agents leverage Cloudflare Durable Objects to maintain persistent, isolated chat sessions by binding each WebSocket connection to a unique SamChatAgent instance that caches session context and persists conversation history across client reconnects.
The OpenSEO platform (every-app/open-seo) implements a Search-and-Assist Metadata (SAM) system that transforms stateless AI interactions into persistent conversations. Unlike traditional serverless functions that lose context between requests, SAM agents utilize Cloudflare Durable Objects (DOs) to maintain continuous state, enabling sophisticated multi-turn conversations with billing controls and project-scoped knowledge.
Architecture Overview
Session Binding and WebSocket Connections
When a user initiates a chat, the client establishes a WebSocket connection using the useAgent hook with the session identifier. The Worker authorizes the connection and routes the request to the Durable Object whose instance name matches the chat-session UUID. This binding ensures that all messages for a specific session route to the same physical compute instance throughout the conversation lifecycle.
In src/env.d.ts, the TypeScript declarations expose the SAM_CHAT Durable Object namespace to the worker, while wrangler.jsonc binds this namespace to the actual DO implementation. This configuration enables the routing layer to instantiate SamChatAgent objects on-demand based on the session ID provided in the WebSocket handshake.
The SamChatAgent Durable Object Class
The core implementation resides in src/server/features/sam/SamChatAgent.ts, where the SamChatAgent class extends the Think framework (extends Think). Each instance represents a single chat session and lives for the duration of that conversation. The class encapsulates storage management, security validation, and LLM orchestration within a single persistent object.
State Management Patterns
Per-Session Context Caching
Upon the first interaction, the Durable Object loads the session metadata from the database using SamSessionRepository.getSessionById(this.name) and retrieves the associated project configuration via ProjectRepository.getProjectById(row.projectId). This context—including the session row, project details, and user email—is cached in this.samContext for the object's lifetime, eliminating redundant database queries on subsequent turns.
Persistent Storage with this.ctx.storage
All chat messages and metadata are stored within the Durable Object's internal storage via this.ctx.storage. When the DO hibernates due to inactivity, Cloudflare automatically persists this storage and restores it on the next request. This mechanism guarantees that conversation history survives client disconnects, browser refreshes, or network interruptions without requiring external database writes for every message exchange.
Project-Wide Shared Memory
SAM agents access a "project_context" block that reflects shared AI context across the entire project. The renderProjectContext() method generates this block on-demand, making consistent knowledge available to every turn within any session for that project. This shared memory layer ensures that all conversations within a project maintain alignment on core facts and configuration, even as individual sessions remain isolated.
Security and Billing Controls
Access Gating with beforeTurn
Before processing each message, the beforeTurn method validates the user's credit balance and organization membership status. If credits are exhausted or the user no longer belongs to the organization, the turn is immediately refused. This gating occurs within the Durable Object itself in src/server/features/sam/SamChatAgent.ts, ensuring that billing checks happen at the compute layer closest to the conversation state, preventing unauthorized LLM inference usage.
Conversation Lifecycle
Toolsets and System Prompts
Each conversation turn receives a customized toolset built by buildSamMcpTools() and a system prompt generated by buildSamSystemPrompt() based on the project's data. These components are constructed fresh for each interaction, allowing dynamic tool availability while maintaining the persistent session state within the Durable Object.
Rewinding and Archiving Sessions
The Durable Object exposes lifecycle APIs for conversation management. A POST request to /rewind removes a specific message and its descendants from the conversation tree, then clears the stored terminal state. This allows users to undo interactions and resume from earlier points in the conversation without creating new sessions. Session creation and archival are handled by server functions defined in src/serverFunctions/sam.ts, which interact with SamSessionRepository for database persistence.
Implementation Examples
Create a new SAM chat session on the server:
// src/serverFunctions/sam.ts
export const createSamSession = createServerFn({ method: "POST" })
.middleware(requireProjectContext)
.handler(async ({ context }) => {
const session = await SamSessionRepository.createSession({
projectId: context.projectId,
userId: context.userId,
});
return { id: session.id };
});
Connect to the Durable Object from the client:
// Client-side React component
import { useAgent } from "agents/react";
const { sendMessage, messages } = useAgent({
name: sessionId, // UUID returned from createSamSession
type: "SAM_CHAT", // Matches Durable Object binding in wrangler.jsonc
});
Rewind (undo) the last user message:
// Client-side fetch
await fetch(`/api/sam/${sessionId}/rewind`, {
method: "POST",
body: JSON.stringify({ messageId: lastUserMessageId }),
});
Internal context loading within the Durable Object:
// Inside SamChatAgent.ts
private async loadSamContext(): Promise<SamContext | null> {
if (this.samContext) return this.samContext;
const row = await SamSessionRepository.getSessionById(this.name);
if (!row) return null;
const project = await ProjectRepository.getProjectById(row.projectId);
if (!project) return null;
// Resolve user email, then cache
this.samContext = { row, project, userEmail: creator.email };
return this.samContext;
}
Summary
- SAM agents use Cloudflare Durable Objects to create isolated, persistent chat sessions bound to unique session UUIDs
- Session context loads once per DO lifetime from
SamSessionRepositoryandProjectRepository, then caches inthis.samContext this.ctx.storageautomatically persists conversation history across hibernation cycles and client reconnects- The
beforeTurnmethod enforces billing and access controls before processing each message - Project-wide context sharing via
renderProjectContext()maintains consistent knowledge across all sessions - Lifecycle APIs like
/rewindenable conversation management without session destruction
Frequently Asked Questions
What is the relationship between SAM agents and Durable Objects?
Each SAM chat session corresponds to exactly one Durable Object instance of the SamChatAgent class defined in src/server/features/sam/SamChatAgent.ts. The DO provides the persistent compute environment where the agent maintains state, executes tool calls, and manages the conversation lifecycle independently of other sessions.
How does session state survive browser refreshes?
State persists through Cloudflare's this.ctx.storage API, which automatically saves the DO's memory when it hibernates due to inactivity. When the client reconnects via WebSocket using the same session ID, the platform routes the connection to the existing DO instance, restoring all previous context and conversation history without data loss.
Where are billing checks implemented in the SAM architecture?
Billing validation occurs in the beforeTurn method within src/server/features/sam/SamChatAgent.ts. This method queries the user's credit balance and organization membership before allowing the LLM inference to proceed, ensuring accurate per-turn access control and preventing usage beyond allocated limits.
Can multiple chat sessions share project-level context?
Yes. While each session maintains isolated conversation history in its own Durable Object instance, all sessions access project context through renderProjectContext(), which retrieves shared project configuration from the database. This architecture allows consistent AI behavior across multiple concurrent chats within the same project while keeping individual conversation histories separate.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →