AI Agents Architecture

AI Agents — Cloudflare Agents SDK, Persistent State, Multi-user WebSocket, EU Jurisdiction AI Agents Architecture Agents SDK Persistent State Multi-user WebSocket EU Jurisdiction

Overview

The platform provides AI-powered educational services through the Cloudflare Agents SDK, which extends Durable Objects with conversation management, WebSocket streaming, persistent state, and typed RPC. Each agent is a Durable Object instance with its own SQLite database, capable of handling multiple simultaneous WebSocket connections.

AI agents serve four educational use cases:

  • Educational Assistant — General Q&A, curriculum navigation, learning path recommendations.
  • Tutoring Agent — Subject-specific tutoring with Socratic method, step-by-step explanations, and adaptive difficulty.
  • Content Generation Agent — Lesson plans, quizzes, rubrics, and study materials for teachers.
  • Assessment Agent — Automated grading, feedback generation, and learning gap analysis.

The agent infrastructure builds on the platform's existing Cloudflare investments:


Architecture

Agent Architecture — Browser through Worker to Agent Durable Object to AI Model Provider Agent Architecture Browser mfe-agents useAgentChat hook CustomEvents bridge WebSocket Agent Service Worker • Route /api/agents/:type/:id • WebSocket upgrade • DO stub.fetch() stub Agent DO AIChatAgent subclass SQLite state Conversations @callable() RPC Tool calling jurisdiction("eu") AI Model Provider OpenAI / Anthropic / Workers AI EU endpoint preferred HTTPS API Gateway JWT · CORS · Rate limit → AGENT_SERVICE binding HTTP (REST API) EU Data Residency Agent DO state (SQLite), conversation history, and all PII remain in EU data centers via jurisdiction("eu")

Data flow:

  1. The browser opens a WebSocket connection to the Agent Service Worker via the API Gateway.
  2. The Agent Service Worker upgrades the connection, resolves the agent DO by type and ID, and forwards the request to the DO stub.
  3. The Agent DO (an AIChatAgent subclass) accepts the WebSocket, manages conversation state in its local SQLite database, and streams responses from the AI model provider.
  4. The AI Model Provider (OpenAI, Anthropic, or Cloudflare Workers AI) receives the prompt and streams tokens back to the agent, which forwards them to all connected clients via WebSocket.

Why Cloudflare Agents SDK

CapabilityAgents SDKCustom DOsExternal AI Service
Persistent conversation stateBuilt-in (agent-local SQLite)Manual implementationExternal DB required
Multi-user WebSocketBuilt-in with hibernationManual implementationNot supported
Typed RPC (@callable())Built-inAvailable (WorkerEntrypoint)N/A
React hooksuseAgentChat includedManual implementationVendor-specific
Tool calling / MCPNative supportManual implementationVendor-specific
EU jurisdiction pinningjurisdiction("eu") (DO-based)jurisdiction("eu")Depends on vendor
Streaming responsesBuilt-in (streamText)Manual SSE/WSVendor-specific
Scheduling (alarms)Built-inAvailable (DO alarms)External scheduler
InfrastructureUses existing CF accountUses existing CF accountSeparate vendor
Cost modelDO pricing (pay for state duration)DO pricingPer-query SaaS pricing

The Agents SDK eliminates the need to build conversation management, WebSocket routing, and state persistence from scratch. Since the platform already uses Durable Objects for real-time features, the Agents SDK is a natural extension with no new infrastructure dependencies.


Agent Types

Educational Assistant

The general-purpose agent for day-to-day educational support. It handles:

  • Curriculum navigation and content search
  • Learning path recommendations based on student progress
  • General Q&A about course material
  • Scheduling and deadline reminders (via DO alarms)
// workers/agent-service/src/agents/assistant.ts
import { AIChatAgent } from "agents/ai-chat-agent";
import { type StreamTextOnFinishCallback, type ToolSet, streamText } from "ai";

interface AssistantState {
  studentId: string;
  courseIds: string[];
  preferredLanguage: "en" | "es";
}

export class EducationalAssistant extends AIChatAgent<Env, AssistantState> {
  async onChatMessage(onFinish: StreamTextOnFinishCallback<ToolSet>) {
    const result = streamText({
      model: this.getModel(),
      system: `You are an educational assistant for the A2R Workspaces platform.
        Student ID: ${this.state.studentId}
        Enrolled courses: ${this.state.courseIds.join(", ")}
        Respond in: ${this.state.preferredLanguage}
        Be helpful, encouraging, and age-appropriate.`,
      messages: this.messages,
      tools: this.getAssistantTools(),
      onFinish,
    });
    return result;
  }

  private getModel() {
    // Use a cost-effective model for general Q&A
    return this.env.AI_PROVIDER === "openai"
      ? createOpenAI({ apiKey: this.env.OPENAI_API_KEY })("gpt-4o-mini")
      : createAnthropic({ apiKey: this.env.ANTHROPIC_API_KEY })("claude-haiku-4-5-20251001");
  }

  private getAssistantTools(): ToolSet {
    return {
      searchCurriculum: {
        description: "Search course curriculum for relevant content",
        parameters: z.object({ query: z.string(), courseId: z.string() }),
        execute: async ({ query, courseId }) => {
          // Call curriculum BFF via service binding or HTTP
          return { results: [] };
        },
      },
      getStudentProgress: {
        description: "Get the student's progress in a course",
        parameters: z.object({ courseId: z.string() }),
        execute: async ({ courseId }) => {
          return { progress: 0.65, lastActivity: "2026-03-01" };
        },
      },
    };
  }
}

Tutoring Agent

The tutoring agent provides subject-specific, one-on-one tutoring with Socratic method, step-by-step problem solving, and adaptive difficulty:

// workers/agent-service/src/agents/tutoring.ts
import { AIChatAgent } from "agents/ai-chat-agent";
import { type StreamTextOnFinishCallback, type ToolSet, streamText } from "ai";

interface TutoringState {
  studentId: string;
  subject: string;
  difficultyLevel: "beginner" | "intermediate" | "advanced";
  sessionStartedAt: string;
}

export class TutoringAgent extends AIChatAgent<Env, TutoringState> {
  async onChatMessage(onFinish: StreamTextOnFinishCallback<ToolSet>) {
    const result = streamText({
      model: this.getModel(),
      system: `You are a ${this.state.subject} tutor for a ${this.state.difficultyLevel} student.
        Use the Socratic method: ask guiding questions rather than giving answers directly.
        Break complex problems into steps. Celebrate progress. Be patient and encouraging.
        If the student is stuck for more than 2 attempts, provide a hint.
        Never provide complete solutions without the student attempting first.`,
      messages: this.messages,
      tools: this.getTutoringTools(),
      onFinish,
    });
    return result;
  }

  private getModel() {
    // Use a capable model for tutoring (requires reasoning)
    return createOpenAI({ apiKey: this.env.OPENAI_API_KEY })("gpt-4o");
  }

  private getTutoringTools(): ToolSet {
    return {
      generatePracticeExercise: {
        description: "Generate a practice exercise at the student's level",
        parameters: z.object({
          topic: z.string(),
          difficulty: z.enum(["easy", "medium", "hard"]),
        }),
        execute: async ({ topic, difficulty }) => {
          return { exercise: `Practice: ${topic} (${difficulty})`, hints: [] };
        },
      },
      recordProgress: {
        description: "Record that the student completed a concept",
        parameters: z.object({ concept: z.string(), mastery: z.number().min(0).max(1) }),
        execute: async ({ concept, mastery }) => {
          // Persist progress to agent state
          await this.setState({
            ...this.state,
            difficultyLevel: mastery > 0.8 ? "advanced" : mastery > 0.5 ? "intermediate" : "beginner",
          });
          return { recorded: true };
        },
      },
    };
  }
}

Content Generation Agent

Used by teachers and administrators to generate educational materials:

// workers/agent-service/src/agents/content.ts
import { AIChatAgent } from "agents/ai-chat-agent";
import { type StreamTextOnFinishCallback, type ToolSet, streamText } from "ai";

interface ContentState {
  teacherId: string;
  subject: string;
  gradeLevel: string;
  generatedContent: Array<{ type: string; title: string; createdAt: string }>;
}

export class ContentGenerationAgent extends AIChatAgent<Env, ContentState> {
  async onChatMessage(onFinish: StreamTextOnFinishCallback<ToolSet>) {
    const result = streamText({
      model: createOpenAI({ apiKey: this.env.OPENAI_API_KEY })("gpt-4o"),
      system: `You are a curriculum content generator for ${this.state.subject}, grade level ${this.state.gradeLevel}.
        Generate high-quality, standards-aligned educational content.
        Always include learning objectives, assessment criteria, and differentiation suggestions.
        Format output in Markdown for easy editing by the teacher.`,
      messages: this.messages,
      tools: this.getContentTools(),
      onFinish,
    });
    return result;
  }

  private getContentTools(): ToolSet {
    return {
      generateLessonPlan: {
        description: "Generate a structured lesson plan",
        parameters: z.object({
          topic: z.string(),
          duration: z.number().describe("Duration in minutes"),
          standards: z.array(z.string()).optional(),
        }),
        execute: async ({ topic, duration, standards }) => {
          return { type: "lesson_plan", topic, duration };
        },
      },
      generateQuiz: {
        description: "Generate a quiz with answer key",
        parameters: z.object({
          topic: z.string(),
          questionCount: z.number(),
          questionTypes: z.array(z.enum(["multiple_choice", "short_answer", "true_false"])),
        }),
        execute: async ({ topic, questionCount, questionTypes }) => {
          return { type: "quiz", topic, questionCount };
        },
      },
    };
  }
}

Assessment Agent

Provides automated grading and feedback. Requires human oversight for high-stakes assessments:

// workers/agent-service/src/agents/assessment.ts
import { AIChatAgent } from "agents/ai-chat-agent";
import { type StreamTextOnFinishCallback, type ToolSet, streamText } from "ai";

interface AssessmentState {
  teacherId: string;
  rubricId: string;
  requiresReview: boolean; // true for grades that count toward final scores
}

export class AssessmentAgent extends AIChatAgent<Env, AssessmentState> {
  async onChatMessage(onFinish: StreamTextOnFinishCallback<ToolSet>) {
    const systemPrompt = this.state.requiresReview
      ? `You are an assessment assistant. Provide feedback and suggested grades.
         IMPORTANT: All grades are SUGGESTIONS that require teacher review before being recorded.
         Always explain your reasoning for each grade suggestion.`
      : `You are an assessment assistant for formative (non-graded) feedback.
         Provide detailed, constructive feedback to help students learn.`;

    const result = streamText({
      model: createOpenAI({ apiKey: this.env.OPENAI_API_KEY })("gpt-4o"),
      system: systemPrompt,
      messages: this.messages,
      tools: this.getAssessmentTools(),
      onFinish,
    });
    return result;
  }

  private getAssessmentTools(): ToolSet {
    return {
      gradeSubmission: {
        description: "Grade a student submission against a rubric",
        parameters: z.object({
          submissionText: z.string(),
          rubricCriteria: z.array(z.object({
            criterion: z.string(),
            maxPoints: z.number(),
          })),
        }),
        execute: async ({ submissionText, rubricCriteria }) => {
          return {
            status: this.state.requiresReview ? "pending_review" : "formative",
            feedback: [],
          };
        },
      },
    };
  }
}

Agent Implementation

AIChatAgent Base Class

All agents extend AIChatAgent from the Cloudflare Agents SDK. This base class provides:

  • Conversation history stored automatically in agent-local SQLite
  • Multi-user WebSocket connections with hibernation support
  • @callable() typed RPC for non-chat interactions
  • State management via this.state and this.setState()
import { AIChatAgent } from "agents/ai-chat-agent";
import { type Connection, type WSMessage } from "agents";

// AIChatAgent<Env, State> extends the base Agent class
// Env: Worker environment bindings (secrets, service bindings, etc.)
// State: Agent-specific state persisted in SQLite
export class MyAgent extends AIChatAgent<Env, MyState> {
  // Called when a chat message arrives via WebSocket
  async onChatMessage(onFinish: StreamTextOnFinishCallback<ToolSet>) {
    // this.messages — full conversation history (auto-persisted)
    // this.state — agent-specific state
    // this.env — Worker environment bindings
    const result = streamText({ model, system, messages: this.messages, onFinish });
    return result;
  }

  // Called when a new WebSocket client connects
  async onConnect(connection: Connection) {
    console.log(`Client connected: ${connection.id}`);
  }

  // Called when a client disconnects
  async onDisconnect(connection: Connection) {
    console.log(`Client disconnected: ${connection.id}`);
  }
}

State Management and Persistence

Each agent instance has its own SQLite database (part of the Durable Object). State is managed via this.state (read) and this.setState() (write):

// Reading state
const studentId = this.state.studentId;

// Updating state (persisted to SQLite automatically)
await this.setState({
  ...this.state,
  difficultyLevel: "advanced",
  lastInteractionAt: new Date().toISOString(),
});

Conversation history is managed by the AIChatAgent base class and stored in the agent's SQLite database. The SDK handles serialization, pagination, and retrieval automatically.

@callable() Typed RPC

The @callable() decorator exposes agent methods as typed RPC endpoints over WebSocket. This is used for non-chat interactions like retrieving agent state, updating configuration, or triggering actions:

import { AIChatAgent } from "agents/ai-chat-agent";
import { callable } from "agents";

export class TutoringAgent extends AIChatAgent<Env, TutoringState> {
  @callable()
  async getSessionSummary(): Promise<SessionSummary> {
    return {
      messageCount: this.messages.length,
      duration: Date.now() - new Date(this.state.sessionStartedAt).getTime(),
      topicsCovered: this.state.topicsCovered,
    };
  }

  @callable()
  async updateDifficulty(level: "beginner" | "intermediate" | "advanced"): Promise<void> {
    await this.setState({ ...this.state, difficultyLevel: level });
  }

  // Chat messages are handled separately via onChatMessage
  async onChatMessage(onFinish: StreamTextOnFinishCallback<ToolSet>) {
    // ...
  }
}

On the client side, @callable() methods are invoked via the useAgent hook:

import { useAgent } from "agents/react";

function AgentControls({ agentId }: { agentId: string }) {
  const agent = useAgent({ agent: "tutoring-agent", name: agentId });

  const handleGetSummary = async () => {
    const summary = await agent.call("getSessionSummary");
    console.log(summary);
  };

  return <button onClick={handleGetSummary}>Get Session Summary</button>;
}

Multi-User WebSocket Connections

Multiple users can connect to the same agent instance simultaneously. The AIChatAgent broadcasts responses to all connected clients:

export class ClassroomAgent extends AIChatAgent<Env, ClassroomState> {
  async onConnect(connection: Connection) {
    // Track connected users
    const users = this.state.connectedUsers || [];
    await this.setState({
      ...this.state,
      connectedUsers: [...users, connection.id],
    });

    // Notify other connected clients
    this.broadcast(JSON.stringify({
      type: "user_joined",
      userId: connection.id,
    }));
  }

  async onDisconnect(connection: Connection) {
    const users = this.state.connectedUsers || [];
    await this.setState({
      ...this.state,
      connectedUsers: users.filter((id) => id !== connection.id),
    });

    this.broadcast(JSON.stringify({
      type: "user_left",
      userId: connection.id,
    }));
  }

  // broadcast() sends to all connected WebSocket clients
  // This is inherited from the Agent base class
}

Frontend Integration

useAgentChat React Hook

The useAgentChat hook from the Agents SDK provides a ready-made integration for the React-based MFE architecture:

// mfe-agents/src/components/ChatInterface.tsx
import { useAgentChat } from "agents/ai-react";

interface ChatInterfaceProps {
  agentType: "tutoring" | "content" | "assessment" | "assistant";
  agentId: string;
}

export function ChatInterface({ agentType, agentId }: ChatInterfaceProps) {
  const {
    messages,
    input,
    handleInputChange,
    handleSubmit,
    isLoading,
    error,
  } = useAgentChat({
    agent: `${agentType}-agent`,
    name: agentId,
  });

  return (
    <div className="chat-container">
      <div className="messages">
        {messages.map((message) => (
          <div key={message.id} className={`message message--${message.role}`}>
            <div className="message__content">{message.content}</div>
          </div>
        ))}
        {isLoading && <div className="message message--loading">Thinking...</div>}
      </div>
      <form onSubmit={handleSubmit} className="chat-input">
        <input
          value={input}
          onChange={handleInputChange}
          placeholder="Type your message..."
          disabled={isLoading}
        />
        <button type="submit" disabled={isLoading || !input.trim()}>
          Send
        </button>
      </form>
      {error && <div className="chat-error">{error.message}</div>}
    </div>
  );
}

MFE Integration Pattern

The mfe-agents micro frontend is a dedicated MFE that owns all agent-related UI. It is loaded via Module Federation and mounted in the shell app when users navigate to agent features.

Exposing the agent UI as a remote:

// mfe-agents/rsbuild.config.ts
import { pluginModuleFederation } from "@module-federation/rsbuild-plugin";

export default {
  plugins: [
    pluginModuleFederation({
      name: "mfe_agents",
      exposes: {
        "./ChatInterface": "./src/components/ChatInterface",
        "./AgentDashboard": "./src/pages/AgentDashboard",
      },
      shared: {
        react: { singleton: true },
        "react-dom": { singleton: true },
      },
    }),
  ],
};

Loading the agent MFE from the shell:

// shell-app/src/routes/agents.tsx
import { lazy, Suspense } from "react";
import { loadRemote } from "@module-federation/enhanced/runtime";

const AgentDashboard = lazy(() => loadRemote("mfe_agents/AgentDashboard"));

export function AgentsRoute() {
  return (
    <Suspense fallback={<div>Loading agents...</div>}>
      <AgentDashboard />
    </Suspense>
  );
}

Event Contracts for Agent Interactions

Other MFEs can trigger agent interactions via CustomEvents, following the platform's existing inter-MFE communication pattern (11-inter-mfe-communication.md):

// packages/event-contracts/src/agent-events.ts

/** Dispatched by any MFE to request an agent chat session */
export interface AgentRequestEvent {
  type: "agent:request";
  payload: {
    agentType: "tutoring" | "content" | "assessment" | "assistant";
    context?: {
      subject?: string;
      courseId?: string;
      documentId?: string;
    };
  };
}

/** Dispatched by mfe-agents when a session is established */
export interface AgentSessionEvent {
  type: "agent:session:started";
  payload: {
    agentId: string;
    agentType: string;
  };
}

/** Dispatched by mfe-agents when a session ends */
export interface AgentSessionEndEvent {
  type: "agent:session:ended";
  payload: {
    agentId: string;
    summary?: string;
  };
}

Example: Triggering a tutoring session from the dashboard MFE:

// mfe-dashboard/src/components/HelpButton.tsx
function HelpButton({ subject, courseId }: { subject: string; courseId: string }) {
  const handleClick = () => {
    window.dispatchEvent(
      new CustomEvent("agent:request", {
        detail: {
          agentType: "tutoring",
          context: { subject, courseId },
        },
      }),
    );
  };

  return <button onClick={handleClick}>Get Help with {subject}</button>;
}

MCP Server Integration

Agents can expose their tools via the Model Context Protocol (MCP), enabling integration with external AI-powered tools and IDEs:

import { AIChatAgent } from "agents/ai-chat-agent";
import { MCPClientManager } from "agents/mcp/client";

export class EducationalAssistant extends AIChatAgent<Env, AssistantState> {
  #mcpClientManager: MCPClientManager | null = null;

  get mcpClientManager(): MCPClientManager {
    if (!this.#mcpClientManager) {
      this.#mcpClientManager = new MCPClientManager("educational-assistant", "1.0.0");
    }
    return this.#mcpClientManager;
  }

  async onChatMessage(onFinish: StreamTextOnFinishCallback<ToolSet>) {
    // Get tools from connected MCP servers
    const mcpTools = await this.mcpClientManager.getTools();

    const result = streamText({
      model: this.getModel(),
      system: this.getSystemPrompt(),
      messages: this.messages,
      tools: {
        ...this.getBuiltInTools(),
        ...mcpTools,
      },
      onFinish,
    });
    return result;
  }
}

This allows agents to dynamically discover and use tools provided by MCP servers — for example, a curriculum database tool, a student records tool, or a grading system tool.


GDPR Compliance for AI Agents

All agent infrastructure complies with GDPR through the mechanisms described in 03-cloudflare-infrastructure.md. This section covers AI-specific GDPR considerations.

Data Residency

All agent Durable Objects use jurisdiction("eu"), ensuring:

  • Conversation history (stored in agent-local SQLite) resides in EU data centers.
  • Agent state (student profiles, session data, assessment results) never leaves the EU.
  • WebSocket connections are terminated at EU-located Durable Object instances.
// All agent DO bindings include jurisdiction: "eu"
{
  "durable_objects": {
    "bindings": [{
      "name": "TUTORING_AGENT",
      "class_name": "TutoringAgent",
      "jurisdiction": "eu"
    }]
  }
}

Data Retention Policies

Data TypeRetention PeriodJustification
Active conversation historyIndefinite (while student enrolled)Needed for continuity of tutoring
Archived conversations2 years after last interactionEducational record retention
Assessment results5 yearsCompliance with educational regulations
Agent state (non-conversation)1 year after last interactionOperational data
Content generation outputsUntil teacher deletesTeacher-owned content

Right to Deletion (GDPR Article 17)

Students and teachers can request deletion of their data. The platform implements this through agent-level cleanup:

// workers/agent-service/src/gdpr/deletion.ts

export async function deleteUserAgentData(
  env: Env,
  userId: string,
  agentBindings: DurableObjectNamespace[],
): Promise<DeletionReport> {
  const deletedAgents: string[] = [];

  for (const binding of agentBindings) {
    // List all agent instances associated with this user
    // Agent IDs follow the pattern: {userId}:{sessionId}
    const id = binding.idFromName(userId);
    const stub = binding.get(id);

    // Request the agent to delete all user data
    const response = await stub.fetch(new Request("http://internal/gdpr/delete", {
      method: "DELETE",
      headers: { "X-User-Id": userId },
    }));

    if (response.ok) {
      deletedAgents.push(userId);
    }
  }

  return {
    userId,
    deletedAgents,
    timestamp: new Date().toISOString(),
  };
}

Right to Data Export (GDPR Article 20)

Users can export their conversation history and agent data in a portable format:

@callable()
async exportUserData(): Promise<ExportData> {
  return {
    conversations: this.messages,
    state: this.state,
    exportedAt: new Date().toISOString(),
    format: "json",
  };
}

EU AI Act Transparency Requirements

The platform complies with the EU AI Act's transparency requirements for AI systems used in education:

  1. Disclosure: All AI-generated content is clearly labeled as AI-produced.
  2. Human oversight: Assessment agents flag results as "suggestions" requiring teacher review for high-stakes grading.
  3. Explainability: Agents provide reasoning for assessment scores and recommendations.
  4. Risk classification: Educational AI is classified as "high-risk" under the EU AI Act. The platform maintains documentation of training data sources, model versions, and output monitoring.

Human Oversight for Assessments

Assessment agents that influence grades require teacher review:

interface AssessmentResult {
  studentId: string;
  score: number;
  feedback: string;
  requiresReview: boolean;  // true for summative assessments
  reviewedBy?: string;      // teacherId after review
  reviewedAt?: string;
  status: "draft" | "pending_review" | "approved" | "rejected";
}

The platform enforces that no AI-generated grade is recorded in the student's official record without explicit teacher approval.


Agent Lifecycle

Creation

Agent instances are created on-demand when a user initiates a session. The agent ID is derived from the user ID and session context:

// Agent ID pattern: {userId}:{agentType}:{contextId}
const agentId = `${userId}:tutoring:${courseId}`;
const id = env.TUTORING_AGENT.idFromName(agentId);
const stub = env.TUTORING_AGENT.get(id);

Scheduling with Alarms

Agents use Durable Object alarms for scheduled tasks:

export class TutoringAgent extends AIChatAgent<Env, TutoringState> {
  async onStart() {
    // Schedule a daily check for inactive sessions
    const tomorrow = new Date();
    tomorrow.setDate(tomorrow.getDate() + 1);
    tomorrow.setHours(2, 0, 0, 0); // 2 AM UTC
    await this.ctx.storage.setAlarm(tomorrow.getTime());
  }

  async alarm() {
    // Check if the session has been inactive for more than 30 days
    const lastInteraction = new Date(this.state.lastInteractionAt);
    const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);

    if (lastInteraction < thirtyDaysAgo) {
      // Archive the conversation and clean up
      await this.archiveAndCleanup();
    } else {
      // Reschedule for tomorrow
      const tomorrow = new Date();
      tomorrow.setDate(tomorrow.getDate() + 1);
      tomorrow.setHours(2, 0, 0, 0);
      await this.ctx.storage.setAlarm(tomorrow.getTime());
    }
  }
}

Hibernation

Agent Durable Objects use WebSocket hibernation. When no messages are being exchanged, the DO hibernates — WebSocket connections remain open but the DO consumes no CPU or memory. This is the same pattern used in the platform's real-time communication layer (07-real-time-communication.md).

  • During hibernation: WebSocket connections stay open, but the DO is evicted from memory. Billing is $0 for idle time.
  • On message: The DO wakes up, restores state from SQLite, processes the message, and may hibernate again.

Cleanup

When an agent is no longer needed (e.g., after data deletion or session archival):

async archiveAndCleanup() {
  // Export conversation to long-term storage (R2) if retention policy requires
  const exportData = await this.exportUserData();
  // ... store in R2 with jurisdiction("eu")

  // Clear agent-local state
  await this.ctx.storage.deleteAll();
}

Wrangler Configuration

Complete wrangler.jsonc for the agent-service Worker:

// workers/agent-service/wrangler.jsonc
{
  "name": "agent-service",
  "main": "src/index.ts",
  "compatibility_date": "2026-02-25",
  "compatibility_flags": ["nodejs_compat"],

  // ── Durable Object bindings (all EU-pinned) ──────────────────────
  "durable_objects": {
    "bindings": [
      {
        "name": "TUTORING_AGENT",
        "class_name": "TutoringAgent",
        "jurisdiction": "eu"
      },
      {
        "name": "CONTENT_AGENT",
        "class_name": "ContentGenerationAgent",
        "jurisdiction": "eu"
      },
      {
        "name": "ASSESSMENT_AGENT",
        "class_name": "AssessmentAgent",
        "jurisdiction": "eu"
      },
      {
        "name": "EDUCATIONAL_ASSISTANT",
        "class_name": "EducationalAssistant",
        "jurisdiction": "eu"
      }
    ]
  },

  // ── DO SQLite migrations ─────────────────────────────────────────
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": [
        "TutoringAgent",
        "ContentGenerationAgent",
        "AssessmentAgent",
        "EducationalAssistant"
      ]
    }
  ],

  // ── Environment variables ────────────────────────────────────────
  "vars": {
    "AI_PROVIDER": "openai",
    "ENVIRONMENT": "production"
  }

  // Secrets set via: wrangler secret put OPENAI_API_KEY
  // Secrets set via: wrangler secret put ANTHROPIC_API_KEY
}

All jurisdiction fields are set to "eu" to ensure agent state, conversation history, and all PII remain in EU data centers.


Cost Considerations

Durable Objects Pricing (Agent Infrastructure)

ResourcePriceEstimated Monthly UsageEstimated Cost
DO Requests$0.15/million~2M requests (agent interactions)~$0.30
DO Duration$12.50/million GB-seconds~500K GB-seconds~$6.25
DO Storage (reads)$0.20/million~1M reads (conversation history)~$0.20
DO Storage (writes)$1.00/million~200K writes (state updates)~$0.20
DO Stored data$0.20/GB-month~2 GB (conversations + state)~$0.40

AI Model API Costs

ProviderModelCost per 1M Input TokensCost per 1M Output Tokens
OpenAIgpt-4o$2.50$10.00
OpenAIgpt-4o-mini$0.15$0.60
AnthropicClaude Haiku 4.5$0.80$4.00
CloudflareWorkers AI (select models)Included (beta)Included (beta)

Cost control strategies:

  • Use smaller models (gpt-4o-mini, Haiku) for simple tasks (educational assistant, content generation templates).
  • Use capable models (gpt-4o) only for reasoning-heavy tasks (tutoring, assessment).
  • Implement per-tenant token budgets to prevent cost runaway.
  • Cache common responses (e.g., FAQ answers) in agent state to avoid repeated model calls.
  • Use DO hibernation to avoid paying for idle agent instances.

DocumentDescription
Architecture OverviewSystem architecture and design principles
ADR-012: Cloudflare Agents SDKDecision record for AI agents
Cloudflare InfrastructureWorkers, storage, service bindings, GDPR compliance
Local DevelopmentAgent service local development
Real-time CommunicationDurable Objects and WebSocket patterns
Inter-MFE CommunicationCustomEvents for agent interactions

References