---
description: Opinionated chat agent framework with built-in tools, persistent memory, lifecycle hooks, streaming, messengers, scheduled tasks, Workflows, and sub-agent RPC.
title: Think
image: https://developers.cloudflare.com/og-docs.png
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt  
> Use this file to discover all available pages before exploring further.

# Think

Last updated Aug 24, 2026|Copy as Markdown|[View as Markdown](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/index.md)|[Agent setup](https://e8aee267.previews.developers.cloudflare.com/agent-setup/)

`@cloudflare/think` lets you build a stateful AI chat agent — one that streams replies, remembers the conversation, and calls tools — by extending a single base class. You provide a model with `getModel()`, and Think wires up the rest of the chat lifecycle for you: the agentic loop (the model calls tools, reads the results, and keeps going until it has an answer), message persistence, streaming, client tools, stream resumption, and extensions — all backed by Durable Object SQLite.

Think works as both a **top-level agent** (WebSocket chat to browser clients via `useAgentChat`) and a **sub-agent** (a child agent that another agent drives over RPC via `chat()`).

New to Cloudflare Agents?

If this is your first agent, start with the [Getting started tutorial](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/getting-started/) for a guided build. For the bigger picture of what agents are and how they run, read [What are agents?](https://e8aee267.previews.developers.cloudflare.com/agents/concepts/what-are-agents/). Think builds on two Cloudflare primitives worth a quick look: [Workers AI](https://e8aee267.previews.developers.cloudflare.com/workers-ai/) provides the model, and each agent instance is a [Durable Object](https://e8aee267.previews.developers.cloudflare.com/durable-objects/) that stores its state. The rest of this section is reference material you can dip into as you need it.

## Quick start

### Install

npmyarnpnpmbun

```
npm i @cloudflare/think @cloudflare/ai-chat agents ai @cloudflare/shell zod workers-ai-provider
```

```
yarn add @cloudflare/think @cloudflare/ai-chat agents ai @cloudflare/shell zod workers-ai-provider
```

```
pnpm add @cloudflare/think @cloudflare/ai-chat agents ai @cloudflare/shell zod workers-ai-provider
```

```
bun add @cloudflare/think @cloudflare/ai-chat agents ai @cloudflare/shell zod workers-ai-provider
```

Think supports AI SDK v6 and v7\. Use `ai@^6` with `@ai-sdk/react@^3`, or use `ai@^7` with `@ai-sdk/react@^4`. Keep the AI SDK packages on matching major versions throughout your project.

### Server

```js
import { Think } from "@cloudflare/think";
import { createWorkersAI } from "workers-ai-provider";
import { routeAgentRequest } from "agents";

export class MyAgent extends Think {
	getModel() {
		return createWorkersAI({ binding: this.env.AI })(
			"@cf/moonshotai/kimi-k2.6",
		);
	}
}

export default {
	async fetch(request, env) {
		return (
			(await routeAgentRequest(request, env)) ||
			new Response("Not found", { status: 404 })
		);
	},
};
```

```ts
import { Think } from "@cloudflare/think";
import { createWorkersAI } from "workers-ai-provider";
import { routeAgentRequest } from "agents";

export class MyAgent extends Think<Env> {
	getModel() {
		return createWorkersAI({ binding: this.env.AI })(
			"@cf/moonshotai/kimi-k2.6",
		);
	}
}

export default {
	async fetch(request: Request, env: Env) {
		return (
			(await routeAgentRequest(request, env)) ||
			new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;
```

That is it. Think handles the WebSocket chat protocol, message persistence, the agentic loop, message sanitization, stream resumption, client tool support, and workspace file tools.

### Client

```js
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";

function Chat() {
	const agent = useAgent({ agent: "MyAgent" });
	const { messages, sendMessage, status } = useAgentChat({ agent });

	return (
		<div>
			{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");
					sendMessage({ text: input.value });
					input.value = "";
				}}
			>
				<input name="input" placeholder="Send a message..." />
				<button type="submit">Send</button>
			</form>
		</div>
	);
}
```

```ts
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/ai-chat/react";

function Chat() {
	const agent = useAgent({ agent: "MyAgent" });
	const { messages, sendMessage, status } = useAgentChat({ agent });

	return (
		<div>
			{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;
					sendMessage({ text: input.value });
					input.value = "";
				}}
			>
				<input name="input" placeholder="Send a message..." />
				<button type="submit">Send</button>
			</form>
		</div>
	);
}
```

### Configuration

```jsonc
{
  "$schema": "./node_modules/wrangler/config-schema.json",
  // Set this to today's date
  "compatibility_date": "2026-08-24",
  "compatibility_flags": [
    "nodejs_compat"
  ],
  "ai": {
    "binding": "AI"
  },
  "durable_objects": {
    "bindings": [
      {
        "class_name": "MyAgent",
        "name": "MyAgent"
      }
    ]
  },
  "migrations": [
    {
      "new_sqlite_classes": [
        "MyAgent"
      ],
      "tag": "v1"
    }
  ]
}
```

```toml
# Set this to today's date
compatibility_date = "2026-08-24"
compatibility_flags = ["nodejs_compat"]

[ai]
binding = "AI"

[[durable_objects.bindings]]
class_name = "MyAgent"
name = "MyAgent"

[[migrations]]
new_sqlite_classes = ["MyAgent"]
tag = "v1"
```

### Tracing

Think uses `wrapAISDK()` internally to instrument model turns, tool calls, and approval lifecycle segments. You do not need to wrap the AI SDK or configure an adapter. To send `invoke_agent`, `chat`, `execute_tool`, and `tool_approval` spans to Workers Observability, turn on Workers traces:

```jsonc
{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "observability": {
    "traces": {
      "enabled": true
    }
  }
}
```

```toml
[observability.traces]
enabled = true
```

Traces appear in the Agents view in the Cloudflare Dashboard. You can inspect conversations and trace timelines. To turn tracing off, set `observability.traces.enabled` to `false`. You do not need to change the Think agent class.

For span attributes, payload controls, exporting traces, and direct AI SDK v6 or v7 setup, refer to [Tracing](https://e8aee267.previews.developers.cloudflare.com/agents/runtime/operations/observability/tracing/).

## Think vs AIChatAgent

Both Think and [AIChatAgent](https://e8aee267.previews.developers.cloudflare.com/agents/communication-channels/chat/chat-agents/) extend `Agent` and speak the same `cf_agent_chat_*` WebSocket protocol. They serve different goals.

**AIChatAgent** is a protocol adapter. You override `onChatMessage` and are responsible for calling `streamText`, wiring tools, converting messages, and returning a `Response`. AIChatAgent handles the plumbing — message persistence, streaming, abort, resume — but the LLM call is entirely your concern.

**Think** is an opinionated framework. It makes decisions for you: `getModel()` returns the model, `getSystemPrompt()` or `configureSession()` sets the prompt, `getTools()` returns tools. The default `onChatMessage` runs the complete agentic loop. You override individual pieces, not the whole pipeline.

| Concern                | AIChatAgent                                                      | Think                                                               |
| ---------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------- |
| **Minimal subclass**   | \~15 lines (wire streamText \+ tools + system prompt + response) | 3 lines (getModel() only)                                           |
| **Storage**            | Flat SQL table                                                   | Session: tree-structured messages, context blocks, compaction, FTS5 |
| **Regeneration**       | Destructive (old response deleted)                               | Non-destructive branching (old responses preserved)                 |
| **Context management** | Manual                                                           | Context blocks with LLM-writable persistent memory                  |
| **Sub-agent RPC**      | Not built in                                                     | chat() with StreamCallback                                          |
| **Programmatic turns** | saveMessages()                                                   | saveMessages(), submitMessages(), continueLastTurn()                |
| **Compaction**         | maxPersistedMessages (deletes oldest)                            | Non-destructive summaries via overlays                              |
| **Search**             | Not available                                                    | FTS5 full-text search per-session and cross-session                 |

### When to use AIChatAgent

* You need full control over the LLM call (RAG, multi-model, custom streaming)
* You want the `Response` return type for HTTP middleware or testing
* You are building a simple chatbot with no memory requirements

### When to use Think

* You want to ship fast (3-line subclass with everything wired)
* You need persistent memory (context blocks the model can read and write)
* You need long conversations (non-destructive compaction)
* You need conversation search (FTS5)
* You are building a sub-agent system (parent-child RPC with streaming)
* You need proactive agents (programmatic turns from scheduled tasks or webhooks)
* You need durable async submission for webhook or RPC callers

## Choose a turn API

Think has several ways to start or continue a turn. They all funnel through one public entry point — `runTurn(options)` — and the older methods remain as convenience shortcuts.

### runTurn()

Experimental

`runTurn()` is stable in shape, but may evolve before Think graduates out of experimental.

`runTurn()` is the unified turn-admission API. One method, three modes, selected by `options.mode`:

| Mode             | Use when                                                     | Returns                       | Shortcut for     |
| ---------------- | ------------------------------------------------------------ | ----------------------------- | ---------------- |
| "wait" (default) | The caller can block until the model response is finished    | Promise<TurnResult>           | saveMessages()   |
| "submit"         | The caller needs fast, durable acceptance and a later status | Promise<SubmitMessagesResult> | submitMessages() |
| "stream"         | The caller wants the response streamed to a callback (RPC)   | Promise<void>                 | chat()           |

The `input` accepts a string, a `UIMessage`, an array of messages, or — in `wait` and `stream` modes — a function `(current) => UIMessage[]` evaluated at admission. (`submit` does not accept function input.)

```js
export class Assistant extends Think {
	async examples(inboundEventId) {
		// wait — block for the result
		const result = await this.runTurn({ input: "Summarize the latest thread" });
		if (result.status === "completed") {
			// result.message is the assistant message; result.continuation is false
		}

		// submit — durable acceptance, check status later
		const submission = await this.runTurn({
			mode: "submit",
			input: "Process this webhook",
			idempotencyKey: inboundEventId, // dedupe; safe to retry
		});
		// submission.accepted is true on first accept; submission.status is "pending"

		// stream — drive a callback (the same surface as chat())
		await this.runTurn({
			mode: "stream",
			input: "Stream me",
			callback: {
				onStart({ requestId }) {},
				onEvent(json) {}, // UIMessageChunk JSON
				onDone() {},
				onError(error) {},
			},
		});

		// continuation — continue the last assistant turn instead of sending input
		await this.runTurn({ continuation: true });
	}
}
```

```ts
export class Assistant extends Think<Env> {
	async examples(inboundEventId: string) {
		// wait — block for the result
		const result = await this.runTurn({ input: "Summarize the latest thread" });
		if (result.status === "completed") {
			// result.message is the assistant message; result.continuation is false
		}

		// submit — durable acceptance, check status later
		const submission = await this.runTurn({
			mode: "submit",
			input: "Process this webhook",
			idempotencyKey: inboundEventId, // dedupe; safe to retry
		});
		// submission.accepted is true on first accept; submission.status is "pending"

		// stream — drive a callback (the same surface as chat())
		await this.runTurn({
			mode: "stream",
			input: "Stream me",
			callback: {
				onStart({ requestId }) {},
				onEvent(json) {}, // UIMessageChunk JSON
				onDone() {},
				onError(error) {},
			},
		});

		// continuation — continue the last assistant turn instead of sending input
		await this.runTurn({ continuation: true });
	}
}
```

Key behaviors:

* **Blocking modes cannot nest.** Calling `wait`/`stream`/`continuation` (or the equivalent shortcut) from _inside_ an active turn — for example, from a tool's `execute` — throws, because it would deadlock the turn queue. From inside a turn, use `runTurn({ mode: "submit" })` (durable, runs after the current turn frees the queue) or [addMessages()](#add-messages-without-a-turn) (transcript only, no inference).
* **`submit` is idempotent.** Pass `submissionId` and/or `idempotencyKey`; re-submitting a known key returns the existing record with `accepted: false` instead of starting a second turn. See [Programmatic submissions](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/programmatic-submissions/).
* **Recovery-safe.** The `wait`, `stream`, and drained `submit` paths run inference inside a recovery fiber, so an interrupted turn resumes after eviction.

`runTurn` is exported alongside its option and result types: `RunTurnOptions`, `RunTurnWait`, `RunTurnSubmit`, `RunTurnStream`, `TurnInputMessages`, and `TurnResult`.

### Pick a shortcut

The table below maps each scenario to the most direct call. Each shortcut has an unchanged signature; reach for them when you want the narrower surface, or use `runTurn()` when you want one mental model.

| Use case                                                       | API                                           |
| -------------------------------------------------------------- | --------------------------------------------- |
| A browser user sends chat messages                             | useAgentChat over the WebSocket chat protocol |
| Server code can wait for the model response                    | saveMessages()                                |
| Server code needs fast durable acceptance and later status     | submitMessages()                              |
| Code should create recurring prompt-driven turns or handlers   | getScheduledTasks()                           |
| Parent code needs direct streaming RPC to a specific child     | subAgent(...).chat()                          |
| A parent delegates work to a retained child agent              | agentTool() or runAgentTool()                 |
| Surround a turn with idempotent app-owned side effects         | startFiber()                                  |
| Coordinate multi-step durable orchestration                    | Workflows                                     |
| Add context or messages without starting a model turn          | addMessages()                                 |
| Advanced subclass or recovery code continues an assistant turn | continueLastTurn()                            |

Use `saveMessages()` when the caller owns the trigger and can wait for the turn to finish. Use [submitMessages()](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/programmatic-submissions/) when timeout ambiguity would make retries unsafe.

### Add messages without a turn

Use `addMessages()` to write to the transcript **without** starting a model turn — for importing prior history or injecting background context the next turn should see:

```js
export class Assistant extends Think {
	async importContext() {
		await this.addMessages([
			{
				id: crypto.randomUUID(),
				role: "user",
				parts: [{ type: "text", text: "Imported context" }],
			},
		]);
	}
}
```

```ts
export class Assistant extends Think<Env> {
	async importContext() {
		await this.addMessages([
			{
				id: crypto.randomUUID(),
				role: "user",
				parts: [{ type: "text", text: "Imported context" }],
			},
		]);
	}
}
```

`addMessages()` appends (or upserts) into the Session tree:

* It does **not** run inference and does **not** enter the turn queue, so it is safe to call from inside a tool's `execute` without deadlocking.
* Array entries are appended **linearly** (each attaches under the previous one), so imported history stays a single path. By default the first message attaches to the latest committed leaf; pass `parentId` to attach elsewhere, or `null` for a root message.
* Appends are **idempotent by message id**. Pass `{ mode: "upsert" }` to update an existing message in place instead.

The supported pattern is "add context, then run a turn": call `addMessages()`, then `runTurn()`.

Use `chat()` for low-level parent-to-child streaming when your code owns forwarding, cancellation, and replay policy. Use [Agents as tools](https://e8aee267.previews.developers.cloudflare.com/agents/runtime/execution/agent-tools/) when a parent model or workflow delegates to a child agent and you want retained child runs, event replay, abort bridging, and UI drill-in.

Use [startFiber()](https://e8aee267.previews.developers.cloudflare.com/agents/runtime/execution/durable-execution/#startfiber) outside Think when the durable unit is an application job around a turn: accepting a webhook once, restoring a serialized channel or thread target, posting a visible reply, or recording app-level recovery policy. Think submissions own conversation admission and turn serialization; managed fibers own external job acceptance, idempotent side effects, and application recovery.

## In this section

### [Getting started](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/getting-started/)

Build a Think agent step by step.

### [Configuration](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/configuration/)

Configuration overrides, dynamic configuration, and Session integration.

### [Tools](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/tools/)

Workspace tools, code execution, browser tools, and extensions.

### [Actions](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/actions/)

Server actions with idempotency, approvals, authorization, and reply attachments.

### [Channels](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/channels/)

Per-channel policy, channel selection, and out-of-band notices.

### [Lifecycle hooks](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/lifecycle-hooks/)

beforeTurn, beforeStep, onStepFinish, onChatResponse, and more.

### [Client tools](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/client-tools/)

Browser-side tools, approvals, and concurrency.

### [Messengers](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/messengers/)

Receive and reply to Chat SDK messenger webhooks.

### [Scheduled tasks](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/scheduled-tasks/)

Declarative recurring prompts and handlers.

### [Workflows](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/workflows/)

Durable model-driven reasoning steps inside Cloudflare Workflows.

### [Sub-agent RPC](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/sub-agents/)

chat() streaming, saveMessages, continueLastTurn, and abort.

### [Programmatic submissions](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/programmatic-submissions/)

Durable turn admission for webhooks and RPC callers.

### [Durable recovery](https://e8aee267.previews.developers.cloudflare.com/agents/harnesses/think/recovery/)

Chat recovery, stream-stall watchdog, and stability detection.

### [Agent Skills](https://e8aee267.previews.developers.cloudflare.com/agents/runtime/execution/agent-skills/)

On-demand instructions, resources, and scripts via getSkills().

## Acknowledgments

Think's design is inspired by [Pi ↗](https://pi.dev).

## Example

### [Assistant example](https://github.com/cloudflare/agents/tree/main/examples/assistant)

Explore a multi-session Think assistant with sub-agent routing, shared workspace, MCP, chat recovery, and GitHub OAuth.

## Related

* [Sessions](https://e8aee267.previews.developers.cloudflare.com/agents/runtime/lifecycle/sessions/) — context blocks, compaction, search, multi-session (the storage layer Think builds on)
* [Sub-agents](https://e8aee267.previews.developers.cloudflare.com/agents/runtime/execution/sub-agents/) — `subAgent()`, `abortSubAgent()`, `deleteSubAgent()` (the base Agent methods for spawning children)
* [Chat agents](https://e8aee267.previews.developers.cloudflare.com/agents/communication-channels/chat/chat-agents/) — `AIChatAgent` for when you need full control over the LLM call
* [Long-running agents](https://e8aee267.previews.developers.cloudflare.com/agents/concepts/agentic-patterns/long-running-agents/) — sub-agent delegation patterns for multi-week agent lifetimes
* [Durable execution](https://e8aee267.previews.developers.cloudflare.com/agents/runtime/execution/durable-execution/) — `runFiber()` and crash recovery (used by `chatRecovery`)
* [Browse the web](https://e8aee267.previews.developers.cloudflare.com/agents/tools/browser/) — full CDP helper API reference

Was this helpful?

YesNo

## On this page

[![](https://e8aee267.previews.developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://e8aee267.previews.developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"WebPage","@id":"https://developers.cloudflare.com/agents/harnesses/think/#page","headline":"Think · Cloudflare Agents docs","description":"Opinionated chat agent framework with built-in tools, persistent memory, lifecycle hooks, streaming, messengers, scheduled tasks, Workflows, and sub-agent RPC.","url":"https://developers.cloudflare.com/agents/harnesses/think/","inLanguage":"en","image":"https://developers.cloudflare.com/og-docs.png","dateModified":"2026-08-24","publisher":{"@type":"Organization","name":"Cloudflare","url":"https://www.cloudflare.com/"},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["AI"]}
```
