AI Agents Architecture
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:
- Durable Objects (07-real-time-communication.md) — Already used for real-time WebSocket features. The Agents SDK extends DO with AI-specific capabilities.
- Service Bindings (03-cloudflare-infrastructure.md) — The API Gateway routes
/api/agentsrequests to theagent-serviceWorker. - Module Federation (02-module-federation.md) — The
mfe-agentsmicro frontend exposes chat UI, agent management, and conversation history. - CustomEvents (11-inter-mfe-communication.md) — Other MFEs can trigger agent interactions via typed events.
Architecture
Data flow:
- The browser opens a WebSocket connection to the Agent Service Worker via the API Gateway.
- The Agent Service Worker upgrades the connection, resolves the agent DO by type and ID, and forwards the request to the DO stub.
- The Agent DO (an
AIChatAgentsubclass) accepts the WebSocket, manages conversation state in its local SQLite database, and streams responses from the AI model provider. - 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
| Capability | Agents SDK | Custom DOs | External AI Service |
|---|---|---|---|
| Persistent conversation state | Built-in (agent-local SQLite) | Manual implementation | External DB required |
| Multi-user WebSocket | Built-in with hibernation | Manual implementation | Not supported |
Typed RPC (@callable()) | Built-in | Available (WorkerEntrypoint) | N/A |
| React hooks | useAgentChat included | Manual implementation | Vendor-specific |
| Tool calling / MCP | Native support | Manual implementation | Vendor-specific |
| EU jurisdiction pinning | jurisdiction("eu") (DO-based) | jurisdiction("eu") | Depends on vendor |
| Streaming responses | Built-in (streamText) | Manual SSE/WS | Vendor-specific |
| Scheduling (alarms) | Built-in | Available (DO alarms) | External scheduler |
| Infrastructure | Uses existing CF account | Uses existing CF account | Separate vendor |
| Cost model | DO pricing (pay for state duration) | DO pricing | Per-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.stateandthis.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 Type | Retention Period | Justification |
|---|---|---|
| Active conversation history | Indefinite (while student enrolled) | Needed for continuity of tutoring |
| Archived conversations | 2 years after last interaction | Educational record retention |
| Assessment results | 5 years | Compliance with educational regulations |
| Agent state (non-conversation) | 1 year after last interaction | Operational data |
| Content generation outputs | Until teacher deletes | Teacher-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:
- Disclosure: All AI-generated content is clearly labeled as AI-produced.
- Human oversight: Assessment agents flag results as "suggestions" requiring teacher review for high-stakes grading.
- Explainability: Agents provide reasoning for assessment scores and recommendations.
- 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
jurisdictionfields 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)
| Resource | Price | Estimated Monthly Usage | Estimated 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
| Provider | Model | Cost per 1M Input Tokens | Cost per 1M Output Tokens |
|---|---|---|---|
| OpenAI | gpt-4o | $2.50 | $10.00 |
| OpenAI | gpt-4o-mini | $0.15 | $0.60 |
| Anthropic | Claude Haiku 4.5 | $0.80 | $4.00 |
| Cloudflare | Workers 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.
Related Documents
| Document | Description |
|---|---|
| Architecture Overview | System architecture and design principles |
| ADR-012: Cloudflare Agents SDK | Decision record for AI agents |
| Cloudflare Infrastructure | Workers, storage, service bindings, GDPR compliance |
| Local Development | Agent service local development |
| Real-time Communication | Durable Objects and WebSocket patterns |
| Inter-MFE Communication | CustomEvents for agent interactions |
References
- Cloudflare Agents SDK Documentation
- Cloudflare Agents SDK — AIChatAgent
- Cloudflare Agents SDK — React Hooks
- Cloudflare Agents SDK — MCP Server
- Cloudflare Durable Objects Documentation
- Cloudflare Durable Objects — Jurisdiction
- Vercel AI SDK — streamText
- EU AI Act Overview
- GDPR Article 17 — Right to Erasure
- GDPR Article 20 — Right to Data Portability