La plupart des frameworks d'agents IA ont un problème de mémoire. Dès qu'une requête se termine, l'agent oublie tout — sa conversation, sa liste de tâches, son travail à moitié achevé. Pour le rendre persistant, vous greffez une base de données, une file d'attente, un serveur WebSocket et un planificateur cron, puis passez des jours à les assembler.
Le Cloudflare Agents SDK emprunte une autre voie. Chaque agent est un Durable Object : une petite instance de calcul mono-thread dotée de sa propre base de données SQLite intégrée, qui vit en périphérie, au plus près de vos utilisateurs. L'état persiste automatiquement entre les requêtes. La même instance peut garder une connexion WebSocket ouverte, exécuter des tâches planifiées, appeler des modèles d'IA et diffuser des mises à jour vers une interface React — sans que vous ayez à provisionner un seul serveur.
Dans ce tutoriel, vous allez créer un agent assistant de recherche persistant. Chaque utilisateur dispose de sa propre instance d'agent qui mémorise ses notes enregistrées, répond aux questions avec un modèle d'IA, journalise chaque interaction dans SQLite, synchronise son état en direct vers une interface React et exécute un récapitulatif quotidien planifié. À la fin, vous maîtriserez tout le modèle d'agent à état et son déploiement sur le réseau Cloudflare.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé
- Un compte Cloudflare gratuit — inscrivez-vous sur dash.cloudflare.com
- Des connaissances de base en TypeScript et React
- Une familiarité avec la notion de fonctions serverless (aucune expérience préalable de Workers n'est requise)
- Un éditeur de code (VS Code recommandé)
Ce que vous allez créer
Un agent assistant de recherche avec :
- Des instances d'agent par utilisateur — chaque instance nommée est un Durable Object isolé
- Un état persistant qui survit aux redémarrages, synchronisé automatiquement vers les clients
- Une journalisation SQLite de chaque question et réponse via la base de données intégrée
this.sql - Des appels de modèles d'IA avec Workers AI (interchangeable avec OpenAI ou tout autre fournisseur)
- Un tableau de bord React en direct câblé avec le hook
useAgent - Un récapitulatif quotidien planifié exécuté selon une expression cron
- Du RPC côté serveur pour piloter les agents depuis des routes API classiques
C'est parti.
Étape 1 : Initialiser le projet
Le moyen le plus rapide est le modèle officiel, mais pour comprendre chaque rouage, nous partirons d'un projet Workers vierge.
npm create cloudflare@latest research-agent -- --type hello-world --ts
cd research-agentChoisissez non à la question « Voulez-vous déployer ? ». Installez maintenant l'Agents SDK :
npm install agentsLe paquet agents vous fournit la classe de base Agent, les utilitaires de routage et le client React. Cette unique dépendance est tout ce dont le runtime a besoin.
Étape 2 : Comprendre l'architecture
Avant d'écrire du code, il est utile de comprendre le modèle qui rend ceci différent d'une fonction serverless ordinaire.
- Une instance par nom. Lorsque vous adressez un agent par un nom — disons
user-42— Cloudflare achemine chaque requête pour ce nom vers la même instance de Durable Object, où qu'elle soit dans le monde. Cette instance est votre agent. - L'état est durable. Chaque instance possède un stockage privé soutenu par SQLite. Tout ce que vous écrivez via
this.setState()outhis.sqlest toujours là à la requête suivante, le lendemain ou au prochain déploiement. - C'est mono-thread. Au sein d'une instance, les requêtes sont traitées une à la fois. Vous obtenez une forte cohérence gratuitement — aucune condition de course sur l'état de votre agent.
- Ça s'éveille et hiberne. Au repos, l'instance hiberne pour économiser des ressources et s'éveille instantanément à l'arrivée d'une nouvelle requête ou d'une tâche planifiée.
C'est ce qui fait qu'un agent ressemble à un objet à longue durée de vie et à état, plutôt qu'à une fonction sans état.
Étape 3 : Définir votre premier agent
Créez src/index.ts et définissez un agent qui détient un état. Chaque agent étend la classe de base Agent avec deux paramètres de type : les liaisons d'environnement et la forme de son état.
import { Agent, routeAgentRequest } from "agents";
// La forme d'une note enregistrée
type Note = {
id: string;
text: string;
createdAt: number;
};
// L'état persistant de l'agent
type ResearchState = {
notes: Note[];
questionsAsked: number;
};
export class ResearchAgent extends Agent<Env, ResearchState> {
// S'exécute une fois quand l'état n'a jamais été défini
initialState: ResearchState = {
notes: [],
questionsAsked: 0
};
// Une méthode RPC que les clients et le serveur peuvent appeler
addNote(text: string) {
const note: Note = {
id: crypto.randomUUID(),
text,
createdAt: Date.now()
};
// setState persiste ET diffuse aux clients connectés
this.setState({
...this.state,
notes: [...this.state.notes, note]
});
return note;
}
deleteNote(id: string) {
this.setState({
...this.state,
notes: this.state.notes.filter((n) => n.id !== id)
});
}
}Deux points à noter. D'abord, initialState ne s'exécute qu'une seule fois par instance — ensuite, this.state correspond à ce que vous avez enregistré en dernier. Ensuite, setState() fait deux choses à la fois : elle persiste le nouvel état dans le stockage et le pousse vers chaque client connecté en temps réel. Vous n'écrivez jamais de code d'abonnement.
Étape 4 : Configurer Wrangler
Cloudflare doit savoir que ResearchAgent est un Durable Object. Ouvrez wrangler.jsonc et ajoutez les liaisons, une migration et la liaison Workers AI que nous utiliserons plus tard.
{
"name": "research-agent",
"main": "src/index.ts",
"compatibility_date": "2026-06-01",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{ "name": "ResearchAgent", "class_name": "ResearchAgent" }
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["ResearchAgent"]
}
],
"ai": {
"binding": "AI"
}
}Le bloc migrations est ce que les débutants oublient. Chaque classe d'agent doit figurer sous new_sqlite_classes, sinon vous rencontrerez une erreur No such Durable Object class à l'exécution. La liaison ai donne à l'agent l'accès aux modèles Workers AI.
Étape 5 : Ajouter le point d'entrée du Worker
Un Worker a besoin d'un export par défaut avec un gestionnaire fetch. L'utilitaire routeAgentRequest inspecte l'URL entrante et l'achemine automatiquement vers la bonne instance d'agent. Ajoutez ceci en bas de src/index.ts :
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Achemine /agents/:agent/:name vers l'instance correspondante
const response = await routeAgentRequest(request, env);
return response ?? new Response("Not found", { status: 404 });
}
} satisfies ExportedHandler<Env>;Avec cela, les requêtes vers /agents/research-agent/user-42 sont dirigées vers l'instance ResearchAgent nommée user-42. Le nommage est automatique : la classe ResearchAgent devient le chemin research-agent en kebab-case.
Lancez le serveur de développement pour confirmer que tout est bien câblé :
npx wrangler devVous devriez voir un serveur local démarrer sans erreur de migration. Laissez-le tourner.
Étape 6 : Persister des données structurées avec SQLite
setState est parfait pour l'état de travail de l'agent, mais pour un historique en ajout seul — chaque question jamais posée — vous voulez une vraie table. Chaque instance d'agent possède sa propre base de données SQLite exposée via le template balisé this.sql. Ajoutez un hook de cycle de vie et une méthode de journalisation à l'agent :
export class ResearchAgent extends Agent<Env, ResearchState> {
initialState: ResearchState = { notes: [], questionsAsked: 0 };
// onStart s'exécute au démarrage de l'instance ou à son réveil d'hibernation
async onStart() {
this.sql`
CREATE TABLE IF NOT EXISTS history (
id TEXT PRIMARY KEY,
question TEXT NOT NULL,
answer TEXT NOT NULL,
created_at INTEGER NOT NULL
)
`;
}
private logInteraction(question: string, answer: string) {
this.sql`
INSERT INTO history (id, question, answer, created_at)
VALUES (
${crypto.randomUUID()},
${question},
${answer},
${Date.now()}
)
`;
}
getHistory() {
return this.sql`
SELECT * FROM history
ORDER BY created_at DESC
LIMIT 50
`;
}
}Le template this.sql interpole les valeurs en toute sécurité sous forme de paramètres liés, il n'y a donc aucun risque d'injection SQL. Comme la table vit à l'intérieur du Durable Object, les requêtes sont locales et rapides — sans aller-retour réseau vers une base distante.
Astuce : Utilisez
setStatepour le petit état réactif que l'interface affiche directement, etthis.sqlpour les jeux de données plus volumineux ou en ajout seul que vous interrogez à la demande. Combiner les deux est le modèle idiomatique.
Étape 7 : Appeler un modèle d'IA à l'intérieur de l'agent
Faites maintenant en sorte que l'agent fasse réellement de la recherche. Ajoutez une méthode ask qui appelle un modèle Workers AI, met à jour le compteur dans l'état et journalise l'échange dans SQLite.
async ask(question: string): Promise<string> {
// Appel d'un modèle Workers AI via la liaison env
const result = await this.env.AI.run("@cf/meta/llama-3.3-70b-instruct", {
messages: [
{
role: "system",
content: "You are a concise research assistant. Answer in 3 sentences."
},
{ role: "user", content: question }
]
});
const answer = result.response ?? "No answer generated.";
// Mise à jour de l'état réactif — les clients voient le nouveau compteur instantanément
this.setState({
...this.state,
questionsAsked: this.state.questionsAsked + 1
});
// Persistance de l'échange complet dans la table d'historique de l'agent
this.logInteraction(question, answer);
return answer;
}Comme ceci s'exécute à l'intérieur de l'agent, l'appel au modèle, la mise à jour de l'état et l'écriture SQLite partagent tous la même instance cohérente. Si vous préférez OpenAI, Anthropic ou un autre fournisseur, remplacez l'appel this.env.AI.run par le SDK de ce fournisseur — le reste de la méthode est inchangé.
Étape 8 : Construire un tableau de bord React en direct
C'est ici que le modèle prend tout son sens. Le hook useAgent de agents/react ouvre un WebSocket vers une instance d'agent nommée et maintient state synchronisé automatiquement. Quand l'agent appelle setState, votre composant se rerend — sans polling, sans fetch manuel.
Créez un composant React (il peut vivre dans une application front séparée ou comme asset statique de Workers) :
import { useAgent } from "agents/react";
import { useState } from "react";
type Note = { id: string; text: string; createdAt: number };
type ResearchState = { notes: Note[]; questionsAsked: number };
export function ResearchDashboard({ userId }: { userId: string }) {
const [draft, setDraft] = useState("");
const [question, setQuestion] = useState("");
const [answer, setAnswer] = useState("");
// Connexion à l'instance d'agent propre à cet utilisateur
const agent = useAgent<ResearchState>({
agent: "research-agent",
name: userId
});
async function handleAsk() {
// Appel de la méthode RPC de l'agent via la connexion ouverte
const reply = await agent.stub.ask(question);
setAnswer(reply);
setQuestion("");
}
return (
<div>
<h2>Questions asked: {agent.state?.questionsAsked ?? 0}</h2>
<input
value={draft}
onChange={(e) => setDraft(e.target.value)}
placeholder="Save a note"
/>
<button onClick={() => { agent.stub.addNote(draft); setDraft(""); }}>
Add note
</button>
<ul>
{(agent.state?.notes ?? []).map((n) => (
<li key={n.id}>
{n.text}
<button onClick={() => agent.stub.deleteNote(n.id)}>x</button>
</li>
))}
</ul>
<input
value={question}
onChange={(e) => setQuestion(e.target.value)}
placeholder="Ask a research question"
/>
<button onClick={handleAsk}>Ask</button>
{answer && <p>{answer}</p>}
</div>
);
}Deux clients connectés au même userId verront les notes l'un de l'autre apparaître instantanément, car setState diffuse vers chaque connexion. L'objet agent.stub est un proxy typé : appeler agent.stub.ask(...) invoque la méthode ask sur le serveur et retourne son résultat.
Étape 9 : Planifier des tâches récurrentes
Une fonctionnalité marquante : les agents peuvent planifier leur propre travail futur. Aucun service cron externe n'est nécessaire — le planificateur vit à l'intérieur de l'instance. Utilisez this.schedule(when, methodName, payload). L'argument when accepte un délai en secondes, un objet Date ou une expression cron.
async onStart() {
this.sql`CREATE TABLE IF NOT EXISTS history (
id TEXT PRIMARY KEY, question TEXT, answer TEXT, created_at INTEGER
)`;
// Exécute un récapitulatif chaque jour à 08:00 (planifié une fois par instance)
await this.schedule("0 8 * * *", "dailyDigest", {});
}
async dailyDigest() {
const rows = this.getHistory();
const count = this.state.questionsAsked;
console.log(`Daily digest: ${count} questions, ${rows.length} logged.`);
// Ici, vous pourriez envoyer un résumé par e-mail, pousser une notification,
// ou appeler le modèle d'IA pour résumer les recherches de la journée.
}À l'heure planifiée, Cloudflare réveille l'instance en hibernation et appelle dailyDigest — même si aucun utilisateur n'est connecté. Cela rend le travail d'arrière-plan, les rappels et les relances triviaux. Vous pouvez aussi planifier des tâches ponctuelles avec await this.schedule(30, "methodName", payload) pour une exécution dans 30 secondes.
Étape 10 : Piloter les agents depuis le serveur avec RPC
Parfois, vous devez interagir avec un agent depuis une route API classique — un webhook, un Worker planifié ou un autre service. Utilisez getAgentByName pour obtenir un stub typé de n'importe quelle instance et appeler ses méthodes directement.
import { getAgentByName } from "agents";
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Une route API classique qui parle à un agent côté serveur
if (url.pathname === "/api/ask") {
const { userId, question } = await request.json<{
userId: string;
question: string;
}>();
const agent = await getAgentByName(env.ResearchAgent, userId);
const answer = await agent.ask(question);
return Response.json({ answer });
}
const response = await routeAgentRequest(request, env);
return response ?? new Response("Not found", { status: 404 });
}
} satisfies ExportedHandler<Env>;getAgentByName(env.ResearchAgent, userId) retourne la même instance à laquelle le tableau de bord React est connecté. Appelez ask depuis le serveur et le résultat — le nouveau compteur de questions — se reflète instantanément dans l'interface ouverte de l'utilisateur, car l'état est partagé entre tous les points d'entrée.
Tester votre implémentation
Démarrez le serveur de développement et sollicitez l'agent :
npx wrangler devEnvoyez ensuite une requête vers la route côté serveur :
curl -X POST http://localhost:8787/api/ask \
-H "Content-Type: application/json" \
-d '{"userId":"user-42","question":"What is edge computing?"}'Vous devriez récupérer une réponse au format JSON. Relancez-la et le compteur questionsAsked de l'agent s'incrémente — la preuve que l'état a persisté entre deux requêtes indépendantes vers la même instance.
Déploiement sur Cloudflare
Le déploiement tient en une seule commande. Wrangler empaquette votre Worker, provisionne les Durable Objects et pousse le tout vers le réseau mondial de Cloudflare :
npx wrangler deployVotre agent s'exécute désormais dans des centaines de localisations à travers le monde, chaque instance se matérialisant au plus près de l'utilisateur qui l'adresse. Aucun serveur à gérer, aucune base de données à provisionner — le stockage SQLite voyage avec chaque Durable Object.
Dépannage
Erreur No such Durable Object class. Votre classe d'agent est absente de la liste new_sqlite_classes dans wrangler.jsonc. Ajoutez-la sous une entrée migrations et redémarrez.
L'état est undefined au premier rendu. agent.state vaut undefined jusqu'à ce que le WebSocket se connecte et que la première synchronisation arrive. Lisez-le toujours avec prudence, par exemple agent.state?.notes ?? [].
La tâche planifiée ne se déclenche jamais. Les expressions cron s'exécutent en UTC. Vérifiez l'expression et assurez-vous que l'instance d'agent a été créée au moins une fois — la planification se produit dans onStart, qui ne s'exécute qu'au démarrage d'une instance.
La liaison AI est indéfinie. Vérifiez que le bloc de liaison ai existe dans wrangler.jsonc et que vous avez redéployé après l'avoir ajouté.
Étapes suivantes
- Ajoutez une validation humaine dans la boucle en mettant en pause au sein d'un appel d'outil et en reprenant à la confirmation de l'utilisateur.
- Diffusez les réponses de l'IA jeton par jeton vers l'interface au lieu de retourner une seule chaîne.
- Explorez la couche agent de chat du SDK pour une gestion complète de la conversation avec historique des messages.
- Combinez les agents avec Cloudflare Workflows pour des pipelines durables multi-étapes.
- Lisez notre guide complémentaire sur l'exécution sécurisée de code pour les agents IA lorsque votre agent doit exécuter du code non fiable.
Conclusion
Le Cloudflare Agents SDK condense la pile habituelle — base de données, file d'attente, serveur WebSocket, planificateur — en une seule primitive : un objet à état, durable, résidant en périphérie. Vous avez écrit une seule classe Agent et obtenu un état persistant, une synchronisation React en direct, une journalisation SQLite, des appels de modèles d'IA, du travail planifié et du RPC côté serveur, puis vous l'avez déployé en une commande.
Le changement de perspective consiste à traiter chaque agent comme un objet à longue durée de vie adressé par son nom, plutôt qu'une fonction sans état. Une fois cela assimilé, créer des expériences persistantes, multijoueurs et propulsées par l'IA devient radicalement plus simple — et tout s'exécute en périphérie, à quelques millisecondes de vos utilisateurs.