Most "durable agent" write-ups end with the same homework assignment: pick a durable-execution engine, wrap every tool call so replay doesn't double-charge a card, stand up a database for history, and find a sandbox for code. Cloudflare's Project Think (@cloudflare/think) is a bet that you shouldn't have to assemble any of that. It's an opinionated base class you extend — it handles the agentic loop, message persistence, streaming, tool execution, durable recovery, and sub-agents, all backed by Durable Object SQLite.

The one-screen version: you write a class that extends Think, override getModel() and getTools(), and you get a crash-recoverable agent with isolated sub-agents and a code sandbox for free. Here's the whole loop.

1. The base class#

Everything hangs off one class. Think lists agents (the Cloudflare Agents SDK), ai (Vercel AI SDK v6), and zod as peer dependencies — so it's a layer on top of the SDK, not a replacement for it.

import { Think } from "@cloudflare/think";

export class MyAgent extends Think<Env> {
  getModel() {
    return "@cf/moonshotai/kimi-k2.7-code";
  }
}

That default model is worth a second look — Cloudflare's own example reaches for a Kimi code model on Workers AI. You can return any model id or a LanguageModel instance.

The wrangler.jsonc is standard Durable Object boilerplate — bind the class, add the SQLite migration, point main at your entry:

{
  "compatibility_date": "2026-01-28",
  "compatibility_flags": ["nodejs_compat"],
  "ai": { "binding": "AI" },
  "durable_objects": {
    "bindings": [{ "class_name": "MyAgent", "name": "MyAgent" }]
  },
  "migrations": [{ "new_sqlite_classes": ["MyAgent"], "tag": "v1" }],
  "main": "src/server.ts"
}

2. Make the turn survive a crash#

This is the feature you'd otherwise reach for Temporal or Inngest to get. Think wraps each chat turn in a recoverable fiber through its chatRecovery config. Per the docs, "an in-flight turn survives Durable Object eviction and resumes; it is not silently lost on deploy or hibernation."

You don't turn it on — it's on. You bound it:

override chatRecovery = {
  maxAttempts: 10,
  terminalMessage: "The assistant was interrupted. Please try again."
};

If recovery runs out of attempts, the user gets terminalMessage instead of a hung stream. The replay trap that bites hand-rolled durable agents — re-running a tool call and sending a second email on recovery — is handled by Think snapshotting replies as accepted, streaming, or completed, so a restart replays only the parts that never reached the client.

3. One entry point, three modes#

Every turn goes through runTurn(). The mode you pass depends on who is waiting for the answer:

// "wait" (default) — block for the result, for interactive chat
const result = await this.runTurn({ input: "Summarize the thread" });

// "submit" — accept durably, poll later. Use this for webhooks.
const submission = await this.runTurn({
  mode: "submit",
  input: "Process webhook",
  idempotencyKey: eventId       // redelivery won't double-run
});

// "stream" — drive a callback (RPC to a client or parent agent)
await this.runTurn({
  mode: "stream",
  input: "Stream me",
  callback: { onEvent(json) {}, onDone() {}, onError(e) {} }
});

The idempotencyKey on submit is the detail that saves you: a webhook that fires twice queues the turn once. Check on it later without re-running anything:

const status = await agent.getSubmissionStatus(submissionId);

4. Sub-agents that can't corrupt the main thread#

A sub-agent is a nested folder under a parent, and Think generates a stable Durable Object class for it — agents/assistant/agents/researcher.ts becomes ThinkSubAgent_Assistant_Researcher. Crucially, each sub-agent gets its own SQLite database. Spin off a long research loop and it can't scribble over the parent's history.

Talk to one over typed RPC — stream it directly, or hand it to the model as a tool:

await subAgent(...).chat({
  input: "Research this topic",
  callback: { onEvent(json) {}, onDone() {} }
});

Use agentTool() instead when you want the model to decide when to delegate, with event replay and abort bridging handled for you.

5. Give it a code sandbox#

Code execution is just a tool. It runs JavaScript, TypeScript, Python, and Bash under Worker Loader, with network and workspace access off by default:

import { createExecuteTool } from "@cloudflare/think/tools/execute";

getTools() {
  return {
    execute: createExecuteTool({
      loader: this.env.LOADER,
      globalOutbound: true      // opt in explicitly
    })
  };
}

For anything sensitive, leave globalOutbound off and prove the sandbox actually blocks the network before you trust it.

6. Persist memory, and pause for a human#

Sessions are tree-structured — regeneration branches history instead of overwriting it — and you attach persistent context the model can read and write:

configureSession(session: Session) {
  return session.withContext("memory", {
    description: "Important facts learned.",
    maxTokens: 2000
  });
}

That's a managed memory block without a vector store. And because tools can pause for approval and resume later without holding a request open, human-in-the-loop stops being a special case — the agent parks, the human clicks, the turn continues, and durability covers the wait.

The trade#

You inspect all of this live — npx @cloudflare/think studio MyAgent alice opens a web console with streaming, tool calls, and approval buttons; npx @cloudflare/think state MyAgent alice prints the transcript without sending a message.

The honest catch is portability: Project Think is Durable Objects, full stop. If you're already on Cloudflare, it deletes the persistence, recovery, and isolation plumbing you'd otherwise write and maintain. If you're not — or you need to run the same agent across clouds — the lower-level Agents SDK or a framework like LangGraph keeps your options open. Pick the batteries-included class when the runtime is the decision.