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.



