écrits/tutorial/2026/08
Tutorial5 août 2026·30 min

Créer des agents IA durables et longue durée avec Cloudflare Project Think et TypeScript

Apprenez à construire des agents IA de production qui survivent aux crashs, à l'hibernation et aux tâches de plusieurs heures avec Cloudflare Project Think. Ce tutoriel couvre la classe Think, les fibers durables, les sous-agents, la mémoire persistante, l'échelle d'exécution et les tâches planifiées en TypeScript.

La plupart des frameworks d'agents IA supposent que l'agent termine son travail au sein d'une seule requête. Le modèle réfléchit, appelle quelques outils, renvoie une réponse, et le processus se termine. Cette hypothèse s'effondre dès que vous demandez à un agent un travail réel — rechercher un sujet à travers cinquante sources, refactoriser une base de code, surveiller un flux de données pendant six heures. Quelque part au milieu, le conteneur est évincé, la connexion tombe, ou la plateforme met votre instance en hibernation, et tout ce que l'agent avait appris disparaît.

Project Think, annoncé lors de l'Agents Week de Cloudflare en août 2026, est la réponse de Cloudflare à ce problème. Il s'agit d'un ensemble de primitives pour agents longue durée — exécution durable, sous-agents, exécution de code en bac à sable et sessions persistantes — accompagné d'une classe de base opiniâtre nommée Think qui relie le tout.

Ce tutoriel construit un véritable agent Think à partir d'un répertoire vide : un assistant de recherche durable doté d'une mémoire persistante, d'outils personnalisés, de travail d'arrière-plan avec points de reprise et de sous-agents délégués.

Note sur la préversion : Project Think est en préversion en août 2026. Cloudflare décrit la surface d'API comme stable mais encore en évolution — attendez-vous à des changements avant une version stable. Considérez-le comme viable en production pour de l'outillage interne et des prototypes, et figez vos versions de dépendances.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20+ avec npm ou pnpm
  • Un compte Cloudflare avec Workers activé (le plan gratuit suffit pour suivre)
  • TypeScript 5.5+ et de l'aisance avec async/await, les classes et les génériques
  • Une familiarité de base avec les Cloudflare Workers et les Durable Objects — nul besoin d'être expert, mais savoir qu'un Durable Object est une instance unique adressable et à état vous aidera
  • Environ 45 minutes

Ce que vous allez construire

À la fin de ce tutoriel, vous aurez déployé un agent qui :

  1. Diffuse les réponses de chat en streaming via WebSockets sans aucun câblage manuel
  2. Retient des faits sur l'utilisateur à travers les redémarrages et l'hibernation
  3. Expose des outils personnalisés aux côtés d'un système de fichiers workspace intégré
  4. Exécute une tâche de recherche en dix étapes qui reprend depuis un point de contrôle si l'instance est évincée
  5. Délègue le travail à des sous-agents isolés fonctionnant en parallèle
  6. Se réveille selon un planning pour produire un briefing quotidien

Pourquoi Think plutôt que le SDK Agents existant

Cloudflare a déjà livré un SDK Agents, et il ne disparaît pas — Think s'appuie dessus. La distinction compte au moment de choisir lequel utiliser.

La classe AIChatAgent d'origine gère le routage et l'appel d'outils de base. Vous câblez vous-même le modèle, le stockage des messages, la boucle de streaming et la gestion d'erreurs — environ quinze lignes de code standard avant que votre agent ne fasse quoi que ce soit d'utile.

Think inverse cela. Il fournit un harnais opiniâtre qui possède l'intégralité du cycle de vie du chat — streaming, persistance, annulation, flux reprenables, gestion d'erreurs, système de fichiers workspace — et ne vous demande de surcharger que ce qui diffère. Un agent Think minimal tient en trois lignes. Par-dessus, il ajoute quatre choses dont AIChatAgent n'a aucun équivalent :

CapacitéCe que cela apporte
Fibers durablesTravail longue durée qui pose des points de contrôle et reprend après un crash
Sous-agentsAgents enfants colocalisés via les Durable Object Facets, chacun avec sa propre base SQLite isolée
Blocs de contexteMémoire persistante, modifiable par le modèle, qui survit à l'hibernation
Échelle d'exécutionCinq paliers de calcul croissants, d'un système de fichiers virtuel jusqu'à un bac à sable complet

Les primitives sont également utilisables séparément. Des paquets comme @cloudflare/codemode, @cloudflare/shell et @cloudflare/worker-bundler fonctionnent sans la classe de base Think si vous voulez les pièces sans les opinions.

Étape 1 : Mise en place du projet

Créez le projet et installez les dépendances.

mkdir research-agent && cd research-agent
npm init -y
npm install @cloudflare/think @cloudflare/ai-chat agents ai @cloudflare/shell zod workers-ai-provider react react-dom
npm install -D wrangler @cloudflare/vite-plugin @cloudflare/workers-types @vitejs/plugin-react @tailwindcss/vite tailwindcss typescript vite

Passons à la configuration du Worker. Créez wrangler.jsonc :

{
  "name": "research-agent",
  "compatibility_date": "2026-01-28",
  "compatibility_flags": ["nodejs_compat"],
  "ai": { "binding": "AI" },
  "assets": {
    "not_found_handling": "single-page-application",
    "run_worker_first": ["/agents/*"]
  },
  "durable_objects": {
    "bindings": [{ "class_name": "ResearchAgent", "name": "ResearchAgent" }]
  },
  "migrations": [{ "new_sqlite_classes": ["ResearchAgent"], "tag": "v1" }],
  "main": "src/server.ts"
}

Trois lignes ici comptent plus que les autres :

  • "ai": { "binding": "AI" } expose l'inférence Workers AI à l'agent via this.env.AI.
  • Le binding durable_objects est ce qui rend l'agent adressable et à état. Chaque conversation obtient sa propre instance.
  • "new_sqlite_classes" dans la migration est obligatoire. Think stocke les messages, sessions, mémoire et points de contrôle des fibers dans la base SQLite du Durable Object. Enregistrer la classe comme un Durable Object simple sans SQLite échouera à l'exécution.

La règle run_worker_first: ["/agents/*"] garantit que les mises à niveau WebSocket des agents atteignent le Worker plutôt que d'être servies comme des ressources statiques.

Ensuite, vite.config.ts :

import { cloudflare } from "@cloudflare/vite-plugin";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
 
export default defineConfig({
  plugins: [react(), cloudflare(), tailwindcss()],
});

Et tsconfig.json, qui étend simplement la configuration fournie par le SDK :

{
  "extends": "agents/tsconfig"
}

Étape 2 : Votre premier agent Think

Créez src/server.ts. C'est l'intégralité du serveur.

import { Think } from "@cloudflare/think";
import { createWorkersAI } from "workers-ai-provider";
import { routeAgentRequest } from "agents";
 
export class ResearchAgent extends Think<Env> {
  getModel() {
    return createWorkersAI({ binding: this.env.AI })(
      "@cf/moonshotai/kimi-k2.6",
    );
  }
 
  getSystemPrompt() {
    return "Tu es un assistant de recherche disposant d'un système de fichiers workspace. Enregistre tes trouvailles dans des fichiers au fur et à mesure.";
  }
}
 
export default {
  async fetch(request: Request, env: Env) {
    return (
      (await routeAgentRequest(request, env)) ||
      new Response("Not found", { status: 404 })
    );
  },
} satisfies ExportedHandler<Env>;

C'est un agent complet. La classe de base Think vous a déjà donné un protocole de chat WebSocket, la persistance des messages en SQLite, le streaming reprenable, les outils de fichiers workspace, la prise en charge de l'annulation et la gestion d'erreurs — rien de tout cela n'apparaît dans votre code.

getModel() est la seule surcharge véritablement obligatoire. Elle renvoie n'importe quel modèle compatible avec l'interface de modèle du Vercel AI SDK, ce qui signifie que Workers AI, OpenAI, Anthropic ou tout ce qui transite par l'AI Gateway de Cloudflare fonctionne ici. getSystemPrompt() est optionnelle ; omettez-la et vous obtenez une valeur par défaut raisonnable.

routeAgentRequest inspecte la requête entrante, la fait correspondre à la bonne instance de Durable Object et transmet la connexion. Si le chemin ne correspond à aucune route d'agent, elle renvoie null — d'où la Response de repli.

Étape 3 : Le client React

Créez src/client.tsx :

import { createRoot } from "react-dom/client";
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";
 
function Chat() {
  const agent = useAgent({ agent: "ResearchAgent" });
  const { messages, sendMessage, status } = useAgentChat({ agent });
 
  return (
    <div>
      <h1>Agent de recherche</h1>
      {messages.map((msg) => (
        <div key={msg.id}>
          <strong>{msg.role}:</strong>
          {msg.parts.map((part, i) =>
            part.type === "text" ? <span key={i}>{part.text}</span> : null,
          )}
        </div>
      ))}
      <form
        onSubmit={(e) => {
          e.preventDefault();
          const input = e.currentTarget.elements.namedItem(
            "input",
          ) as HTMLInputElement;
          if (!input.value.trim()) return;
          sendMessage({ text: input.value });
          input.value = "";
        }}
      >
        <input name="input" placeholder="Demandez-moi une recherche..." />
        <button type="submit">Envoyer</button>
      </form>
      <p>Statut : {status}</p>
    </div>
  );
}
 
const root = document.getElementById("root");
if (root) {
  createRoot(root).render(<Chat />);
}

Notez que messages est un tableau d'objets message dont le contenu vit dans un tableau parts, et non dans une chaîne plate. C'est la forme de message de l'AI SDK v5 — un seul message assistant peut contenir des parties texte, des parties d'appel d'outil et des parties de raisonnement. Ne rendre que part.type === "text" garde cet exemple court ; une vraie interface distinguerait chaque type de partie.

Ajoutez index.html à la racine du projet :

<!doctype html>
<html lang="fr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Agent de recherche</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/client.tsx"></script>
  </body>
</html>

Lancez :

npx vite dev

Envoyez un message. Les réponses arrivent token par token, et le modèle dispose déjà d'outils de fichiers — demandez-lui d'écrire quelque chose dans un fichier et de le relire, il le fera.

Comme Think utilise le même protocole WebSocket que @cloudflare/ai-chat, toute interface de chat existante bâtie sur ce protocole s'intègre sans modification.

Étape 4 : Mémoire persistante avec les blocs de contexte

Le chat en streaming est le minimum syndical. La mémoire qui survit à l'hibernation, c'est là que Think commence à se différencier.

Un Durable Object hiberne lorsqu'il est inactif — l'état en mémoire est effacé et l'instance est reconstruite à la requête suivante. Tout ce que vous aviez stocké sur l'instance de classe a disparu. Les blocs de contexte règlent cela en vivant dans SQLite et en étant réinjectés dans le prompt système à chaque tour.

Surchargez configureSession() dans votre classe d'agent :

import type { Session } from "agents/experimental/memory/session";
 
configureSession(session: Session) {
  return session
    .withContext("soul", {
      provider: {
        get: async () =>
          "Tu es un assistant de recherche. Retiens le domaine de l'utilisateur, ses sources préférées et son style de rédaction.",
      },
    })
    .withContext("memory", {
      description: "Faits importants sur l'utilisateur et ses intérêts de recherche.",
      maxTokens: 2000,
    })
    .withCachedPrompt();
}

Deux types de blocs différents sont à l'œuvre ici.

Le bloc soul est en lecture seule du point de vue du modèle. Son provider.get() s'exécute à chaque tour, vous pouvez donc tirer la valeur d'une base de données, d'un feature flag ou d'une configuration par locataire — c'est un prompt système dynamique, pas une constante.

Le bloc memory est inscriptible. Le déclarer avec une description et un budget maxTokens amène Think à fournir au modèle un outil set_context. Quand l'utilisateur mentionne qu'il travaille dans la fintech et préfère les sources primaires, le modèle peut appeler cet outil, et le fait est persisté en SQLite. La semaine suivante, après cent hibernations de l'instance, le fait est toujours dans le prompt système.

withCachedPrompt() marque le prompt assemblé pour la mise en cache côté fournisseur. Puisque les blocs de contexte se placent en tête stable du prompt tandis que les messages de conversation s'accumulent à la fin, c'est exactement la forme pour laquelle le cache de prompt est conçu — attendez-vous à une réduction de coût notable sur les conversations longues.

Le budget maxTokens a son importance. Une mémoire qui croît sans limite finit par évincer la conversation elle-même. Think impose le plafond et demande au modèle de consolider lorsque le bloc est plein.

Étape 5 : Outils personnalisés

Les outils du système de fichiers workspace sont fournis gratuitement. Les capacités propres à votre domaine vous reviennent. Surchargez getTools() :

import { tool } from "ai";
import { z } from "zod";
import type { ToolSet } from "ai";
 
getTools(): ToolSet {
  return {
    searchPapers: tool({
      description: "Recherche des articles académiques par mot-clé et renvoie titres, auteurs et résumés.",
      inputSchema: z.object({
        query: z.string().describe("Mots-clés de recherche"),
        limit: z.number().min(1).max(25).default(10),
      }),
      execute: async ({ query, limit }) => {
        const res = await fetch(
          `https://api.crossref.org/works?query=${encodeURIComponent(query)}&rows=${limit}`,
        );
        const data = await res.json();
        return data;
      },
    }),
  };
}

Des définitions tool() standard du Vercel AI SDK avec des schémas Zod — rien de spécifique à Think dans la forme.

Ce que Think ajoute, c'est la fusion. Les outils proviennent de sept sources indépendantes : le système de fichiers workspace, la valeur retournée par votre getTools(), les extensions à l'exécution, les outils de session, les skills, les serveurs MCP connectés et les outils côté client enregistrés par le navigateur. Think fusionne l'ensemble en un seul jeu d'outils avant chaque tour. Vous n'assemblez jamais cette liste à la main.

Deux conséquences pratiques. D'abord, les collisions de noms sont réelles — préfixez distinctement vos outils personnalisés si vous connectez aussi des serveurs MCP. Ensuite, le nombre d'outils grimpe vite, et chaque définition d'outil coûte des tokens de prompt à chaque tour. Si vous dépassez la trentaine d'outils, c'est le signal pour vous pencher sur le Code Mode à l'étape 8.

Étape 6 : Exécution durable avec les fibers

Voici la primitive qui justifie l'adoption de Think.

Considérez une tâche de recherche enchaînant dix appels LLM, chacun prenant vingt secondes. Cela fait plus de trois minutes de temps réel. Dans cette fenêtre, le Durable Object peut être évincé pour toutes sortes de raisons — un déploiement, une opération de maintenance de la plateforme, une pression mémoire. Avec une simple méthode async, l'éviction perd tout et l'utilisateur n'obtient rien.

Un fiber est une invocation de fonction durable. Think l'enregistre en SQLite avant que l'exécution ne commence, de sorte que la trace du « ce travail devrait être en cours » survit au processus qui l'exécutait.

async startResearch(topic: string) {
  void this.runFiber("research", async (ctx) => {
    const findings = [];
 
    for (let i = 0; i < 10; i++) {
      const result = await this.callLLM(`Étape de recherche ${i} : ${topic}`);
      findings.push(result);
 
      // Point de contrôle : en cas d'éviction, on reprend ici
      ctx.stash({ findings, step: i, topic });
 
      this.broadcast({ type: "progress", step: i });
    }
 
    return { findings };
  });
}
 
async onFiberRecovered(ctx) {
  if (ctx.name === "research" && ctx.snapshot) {
    const { topic } = ctx.snapshot;
    await this.startResearch(topic);
  }
}

Détaillons chaque pièce.

runFiber(name, fn) enregistre le fiber sous un nom et le démarre. Le préfixe void est délibéré — vous n'attendez pas le résultat. L'appelant retourne immédiatement, et le fiber continue en arrière-plan. Le SDK maintient l'agent en vie pendant toute la durée du fiber automatiquement ; il n'y a ni keepalive manuel ni waitUntil à configurer.

ctx.stash(snapshot) écrit un point de contrôle. Passez-y tout ce dont vous auriez besoin pour reprendre — trouvailles accumulées, index courant, entrée d'origine. Posez un point de contrôle après chaque étape coûteuse, pas à chaque ligne : chaque appel est une écriture SQLite, et jalonner une boucle qui itère mille fois par seconde dominera votre latence.

this.broadcast(message) pousse une mise à jour vers tous les clients WebSocket connectés. C'est ainsi que l'utilisateur suit la progression en temps réel au lieu de fixer un spinner pendant trois minutes.

onFiberRecovered(ctx) est le hook de reprise. Après un crash ou une éviction, Think retrouve les fibers inachevés en SQLite et appelle ce hook avec le dernier stash. C'est vous qui décidez de ce que signifie « reprendre ». L'exemple ci-dessus relance toute la tâche, ce qui est le comportement correct le plus simple mais jette le travail déjà fait. Une meilleure version lit ctx.snapshot.step et reprend à l'index suivant :

async onFiberRecovered(ctx) {
  if (ctx.name !== "research" || !ctx.snapshot) return;
 
  const { topic, findings, step } = ctx.snapshot;
 
  void this.runFiber("research", async (fiberCtx) => {
    const collected = [...findings];
 
    for (let i = step + 1; i < 10; i++) {
      const result = await this.callLLM(`Étape de recherche ${i} : ${topic}`);
      collected.push(result);
      fiberCtx.stash({ findings: collected, step: i, topic });
      this.broadcast({ type: "progress", step: i, resumed: true });
    }
 
    return { findings: collected };
  });
}

Une règle de conception en découle : le corps d'un fiber doit être idempotent depuis le dernier point de contrôle. Si une étape débite une carte bancaire ou envoie un e-mail, cet effet de bord peut se rejouer à la reprise. Placez les opérations non idempotentes juste après un stash, et protégez-les par une clé d'idempotence.

Étape 7 : Les sous-agents

Un agent unique portant cent outils et un prompt système couvrant six domaines fonctionne moins bien que plusieurs agents ciblés. Think rend la délégation peu coûteuse grâce aux Durable Object Facets — des Durable Objects enfants colocalisés avec le parent, chacun avec sa propre base SQLite isolée.

import { Agent } from "agents";
 
export class SearchAgent extends Agent {
  async search(query: string) {
    /* logique de recherche ciblée, outils et prompt propres */
  }
}
 
export class CritiqueAgent extends Agent {
  async analyze(text: string) {
    /* logique de critique ciblée */
  }
}
 
export class Orchestrator extends Agent {
  async handleTask(task: string) {
    const searcher = await this.subAgent(SearchAgent, "search");
    const critic = await this.subAgent(CritiqueAgent, "critique");
 
    const [research, review] = await Promise.all([
      searcher.search(task),
      critic.analyze(task),
    ]);
 
    return this.synthesize(research, review);
  }
}

Le deuxième argument de subAgent() est un nom stable, pas un identifiant aléatoire. Appeler subAgent(SearchAgent, "search") deux fois renvoie la même instance avec le même état accumulé — les sous-agents sont adressables et persistants, pas des workers jetables.

Parce que les facets sont colocalisés avec le parent, les appels RPC sont en pratique des appels de fonction locaux. Il n'y a pas de saut réseau entre Orchestrator et SearchAgent, et c'est ce qui rend le Promise.all ci-dessus véritablement parallèle plutôt que deux allers-retours séquentiels à travers un centre de données.

Chaque sous-agent conserve une base SQLite entièrement séparée. L'agent de critique ne peut pas lire l'historique de conversation de l'agent de recherche sauf si vous le transmettez explicitement. Cette isolation est une fonctionnalité : c'est elle qui maintient la fenêtre de contexte de chaque agent focalisée.

N'oubliez pas d'enregistrer chaque classe de sous-agent dans les migrations de wrangler.jsonc, sinon l'instanciation échoue à l'exécution :

"migrations": [
  {
    "new_sqlite_classes": ["Orchestrator", "SearchAgent", "CritiqueAgent"],
    "tag": "v1"
  }
]

Étape 8 : L'échelle d'exécution et le Code Mode

Think organise le calcul en cinq paliers croissants. Le principe de conception énoncé par Cloudflare est que l'agent doit être utile au palier 0 seul, chaque palier étant purement additif.

PalierEnvironnementPropulsé parCapacité
0Workspace@cloudflare/shellSystème de fichiers virtuel durable sur SQLite et R2 — lecture, écriture, édition, recherche, diff
1Dynamic Worker@cloudflare/codemodeJavaScript généré par le LLM dans un isolat en bac à sable, sans accès réseau
2Résolution NPM@cloudflare/worker-bundlerRécupère et bundle les paquets avec esbuild dans le Dynamic Worker
3NavigateurCloudflare Browser RunNavigation sans interface, clics, extraction, captures d'écran
4Bac à sable completCloudflare SandboxVrai système d'exploitation avec chaînes d'outils — git, npm test, cargo build

Câblez l'échelle dans une configuration d'outils unique :

import { Think } from "@cloudflare/think";
import { createWorkspaceTools } from "@cloudflare/think/tools/workspace";
import { createExecuteTool } from "@cloudflare/think/tools/execute";
import { createBrowserTools } from "@cloudflare/think/tools/browser";
import { createSandboxTools } from "@cloudflare/think/tools/sandbox";
 
export class ResearchAgent extends Think<Env> {
  extensionLoader = this.env.LOADER;
 
  getModel() {
    /* ... */
  }
 
  getTools() {
    return {
      execute: createExecuteTool({
        tools: createWorkspaceTools(this.workspace),
        loader: this.env.LOADER,
      }),
      ...createBrowserTools(this.env.BROWSER),
      ...createSandboxTools(this.env.SANDBOX),
    };
  }
}

Chaque palier au-delà du palier 0 nécessite son propre binding dans wrangler.jsonc — un binding Browser Run pour le palier 3, un binding Sandbox pour le palier 4. N'ajoutez que les paliers dont vous avez réellement besoin ; chacun élargit le rayon d'impact d'un modèle compromis ou confus.

Le Code Mode

createExecuteTool est le plus intéressant, et il change la façon dont le modèle utilise les outils.

La boucle conventionnelle, c'est un appel d'outil par aller-retour au modèle. Trouver les fichiers, attendre. Lire le fichier un, attendre. Lire le fichier deux, attendre. Parcourir cent fichiers coûte cent allers-retours et cent évaluations de prompt.

Le Code Mode remplace cela par un unique programme généré tournant dans un Dynamic Worker en bac à sable :

// Le LLM écrit ceci. Cela s'exécute dans un Dynamic Worker en bac à sable.
const files = await tools.find({ pattern: "**/*.ts" });
const results = [];
for (const file of files) {
  const content = await tools.read({ path: file });
  if (content.includes("TODO")) {
    results.push({ file, todos: content.match(/\/\/ TODO:.*/g) });
  }
}
return results;

Un appel au modèle, une exécution, un résultat. La formule de Cloudflare est que cela réduit « 100 allers-retours vers le modèle » à « une seule exécution de programme », avec l'économie de tokens que cela implique.

C'est le volet sécurité qui rend la chose acceptable. Le code généré s'exécute dans un Dynamic Worker — un isolat neuf sans accès réseau. Il n'atteint le monde extérieur qu'à travers l'objet tools que vous avez passé à createExecuteTool. Du code généré par le modèle et jamais relu s'exécute, certes, mais toute sa surface de capacité se limite au jeu d'outils que vous lui avez explicitement confié.

Étape 9 : Tâches planifiées

Un agent qui n'agit que lorsqu'on lui parle n'est qu'un demi-agent. getScheduledTasks() déclare des tours récurrents :

import { defineScheduledTasks } from "@cloudflare/think";
 
getScheduledTasks() {
  return defineScheduledTasks({
    dailyBriefing: {
      schedule: "every day at 09:00",
      timezone: "Africa/Tunis",
      prompt: "Passe en revue les notes de recherche du workspace et rédige dans briefing.md un résumé de ce qui a changé depuis hier.",
    },
    hourlyCheck: {
      schedule: "every hour",
      handler: async ({ idempotencyKey, scheduledFor }) => {
        // Logique multi-étapes personnalisée au lieu d'un simple prompt
      },
    },
  });
}

Le langage de planification est volontairement lisible : every <n> minutes, every <n> hours, every day at HH:mm, every weekday at HH:mm, every week on monday,wednesday at HH:mm.

Chaque tâche fournit exactement un des deux : prompt ou handler. Un prompt crée une soumission durable qui passe par la boucle agentique normale. Un handler exécute votre propre code et convient aux workflows multi-étapes qui n'ont pas besoin du modèle.

Deux comportements à connaître avant de vous y fier :

Les plannings à heure fixe exigent un fuseau horaire. Tout ce qui comporte une heure précise nécessite un timezone en ligne, un fuseau au niveau de la tâche, ou une surcharge de getDefaultTimezone(). Sans cela, la tâche ne se réconciliera pas. Les plannings relatifs comme every hour en sont exemptés.

Il n'y a pas de rattrapage. Si le Worker était indisponible à l'échéance, Think exécute l'occurrence prévue une seule fois quand l'alarme tardive se déclenche, puis planifie la prochaine exécution future. Un agent hors ligne pendant une semaine ne se réveille pas avec sept briefings en file d'attente.

Les propriétés optionnelles par tâche incluent metadata et retry: { maxAttempts }.

Étape 10 : Hooks de cycle de vie

Think expose quatre points d'interception autour de chaque tour, et c'est là que l'observabilité, les garde-fous et la configuration dynamique ont leur place :

beforeTurn(ctx: TurnContext): TurnConfig | void {
  console.log(`Tour démarré : ${Object.keys(ctx.tools).length} outils disponibles`);
}
 
onChatResponse(result: ChatResponseResult) {
  console.log(`Tour ${result.status} : ${result.message.parts.length} parties`);
}

beforeTurn() s'exécute avant l'invocation du modèle et peut renvoyer un TurnConfig pour surcharger les réglages de ce tour uniquement — basculer vers un modèle moins cher pour les requêtes simples, restreindre le jeu d'outils selon le rôle de l'utilisateur, ajuster le budget de tokens. beforeStep() et onStepFinish() encadrent chaque étape individuelle au sein d'un tour multi-étapes. onChatResponse() se déclenche à la fin du tour, avec ou sans succès.

Remplacez les appels console.log par votre couche de journalisation ou de traçage avant la mise en production — les logs Workers sont éphémères, et les données de tokens et de latence par tour sont exactement ce que vous voudrez avoir quand les coûts vous surprendront.

Tester votre implémentation

Vérifiez chaque couche indépendamment plutôt que de faire confiance à l'ensemble d'un coup.

Streaming et persistance. Lancez npx vite dev, envoyez un message, et confirmez que les tokens arrivent progressivement. Rafraîchissez brutalement le navigateur — la conversation doit se recharger depuis SQLite au lieu de repartir vide.

Mémoire. Communiquez un fait vous concernant à l'agent, puis lancez npx wrangler dev --remote dans une nouvelle session et redemandez-le. Si le fait a disparu, vérifiez que votre bloc de contexte memory déclare bien une description — sans elle, le modèle ne reçoit jamais l'outil set_context et n'a aucun moyen d'écrire.

Reprise de fiber. Démarrez une longue tâche de recherche, puis tuez le serveur de développement en pleine exécution et redémarrez-le. onFiberRecovered doit se déclencher. Ajoutez une ligne de log dans le hook pour le confirmer, et inspectez ctx.snapshot pour vérifier que votre stash contient tout le nécessaire à la reprise.

Sous-agents. Appelez handleTask() et confirmez que les deux sous-agents se terminent. Vérifiez l'isolation en écrivant dans la session d'un sous-agent et en confirmant que l'autre ne peut pas la lire.

Tâches planifiées. Réglez temporairement une tâche sur every 2 minutes avec un handler qui journalise, déployez, et observez npx wrangler tail. Rétablissez ensuite le planning réel.

Déployez quand chaque couche est validée :

npx wrangler deploy

Dépannage

« Cannot use SQL storage on Durable Object class » — la classe est absente de new_sqlite_classes dans vos migrations, ou a été enregistrée comme Durable Object non-SQLite dans un tag de migration antérieur. Chaque classe d'agent et de sous-agent doit y figurer.

La mémoire se réinitialise après une période d'inactivité — vous avez stocké l'état sur l'instance de classe au lieu d'un bloc de contexte. Les propriétés d'instance ne survivent pas à l'hibernation. Tout ce qui doit persister passe par configureSession() ou l'API Session.

Les fibers ne reprennent jamais — confirmez que vous appelez bien ctx.stash() dans le corps du fiber. Un fiber sans point de contrôle n'a rien à reprendre, et onFiberRecovered reçoit un snapshot vide.

Les tâches planifiées ne se déclenchent jamais — presque toujours un fuseau horaire manquant sur un planning à heure fixe. Ajoutez un timezone en ligne ou implémentez getDefaultTimezone().

La connexion WebSocket échoue en production — vérifiez que run_worker_first inclut la route de votre agent. Sans cela, le gestionnaire de ressources statiques intercepte la requête de mise à niveau.

Collisions de noms d'outils — Think fusionne les outils de sept sources. Si un outil personnalisé cesse silencieusement d'être appelé, c'est probablement qu'un serveur MCP ou une extension a enregistré le même nom. Utilisez un espace de noms pour vos outils.

Coûts en tokens plus élevés que prévu — ajoutez withCachedPrompt() si ce n'est pas déjà fait, auditez votre nombre total d'outils, et envisagez de déplacer les séquences d'outils multi-étapes vers le Code Mode.

Prochaines étapes

  • Ajoutez des serveurs MCP. Think fusionne les outils MCP dans le même jeu d'outils : connecter un serveur Model Context Protocol étend l'agent sans toucher à getTools(). Notre tutoriel sur les serveurs MCP explique comment en construire un.
  • Comparez les harnais. Lisez notre tutoriel sur le SDK Cloudflare Agents pour voir ce que Think choisit d'abstraire.
  • Ajoutez une couche Workflows. Pour une orchestration couvrant plusieurs agents et services, Cloudflare Workflows — dont la concurrence a été fortement relevée lors de l'Agents Week — complète les fibers.
  • Durcissez la surface d'outils. Le code généré par le modèle et un accès large aux outils exigent des garde-fous ; voyez notre guide sur les garde-fous d'agents IA et l'injection de prompt.
  • Explorez les extensions auto-rédigées. Les agents Think peuvent écrire leurs propres extensions — des programmes TypeScript s'exécutant dans des Dynamic Workers avec des permissions réseau et workspace déclarées, bundlés et chargés à l'exécution. C'est le coin le plus expérimental de la plateforme, et il mérite d'être suivi.

Conclusion

L'apport de Project Think n'est pas une énième abstraction de chat. C'est la reconnaissance qu'un agent utile est un processus de longue durée, et que les processus de longue durée réclament de l'infrastructure : points de contrôle, reprise, isolation, mémoire persistante et privilèges gradués.

Les trois lignes qui composent un agent Think minimal font le titre, mais c'est le fiber durable qui est la primitive décisive. C'est lui qui sépare un agent qui répond à des questions d'un agent qui abat une heure de travail et survit au redémarrage de la plateforme sous ses pieds.

Le statut de préversion est bien réel — figez vos versions et attendez-vous à des remous d'API avant la stabilisation. Mais la forme générale est juste, et les primitives sous la classe Think restent utilisables séparément si les opinions ne vous conviennent pas.

Chez Noqta, nous construisons des systèmes d'agents IA en production sur Cloudflare et d'autres plateformes edge. Si vous évaluez une architecture d'agents durables pour votre équipe, nous serons ravis d'en discuter.