Skip to content

Tools

Last updated View as MarkdownAgent setup

Think provides built-in workspace file tools on every turn, plus integration points for custom tools, code execution, and dynamic extensions.

Tool merge order

On every turn, Think merges tools from multiple sources. Later sources override earlier ones if names collide:

  1. Workspace tools — read, ls, find, grep, write, edit, delete, plus exec and publish when configured (built-in)
  2. getTools() — your custom server-side tools
  3. Extension tools — tools from loaded extensions (prefixed by extension name)
  4. Session tools — set_context, load_context, search_context (from configureSession)
  5. Skill tools — activate_skill, read_skill_resource, run_skill_script (from getSkills(), refer to Agent Skills)
  6. MCP tools — from connected MCP servers when includeMcpTools is true
  7. Client tools — from the browser (refer to Client tools)

Tools belong to the agent running the turn. For parent-child orchestration, use Agents as tools instead of passing one-off tools through chat().

Built-in workspace tools

Every Think agent gets this.workspace, a Workspace from @cloudflare/computer ↗︎ stored in the Durable Object's SQLite. Think adds Computer's AI SDK tools to every turn with no configuration.

Tool Description
read Read text with line and byte paging; pass images and PDFs to multimodal models
ls List files and directories in a path, with pagination
find Find files matching a glob pattern
grep Search file contents by regex or fixed string
write Write content to a file (creates parent directories)
edit Apply exact text replacements to a file, falling back to whitespace-tolerant search
delete Delete a file or directory
exec Run a command or module on one of the Workspace's execution backends
publish Share a workspace file through a presigned URL, when the Workspace has assets

Paths are absolute. Computer's tools and backends treat /workspace as the working root, and Think writes its own files there too.

@cloudflare/computer is a peer dependency of Think:

npm install @cloudflare/computer

Run code with exec

The default Workspace has no execution backends, so there is no exec tool until you add one. Configure the backends, and the modules they offer, on the Workspace:

import { Think } from "@cloudflare/think";
import { Workspace } from "@cloudflare/computer";
import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript";
import { createGitClient } from "@cloudflare/computer/git";
import { createGitModule } from "@cloudflare/computer/modules/git";

export class MyAgent extends Think {
	workspace = new Workspace({
		storage: this.ctx.storage,
		git: createGitClient(),
		backends: [
			new WorkerJavaScriptBackend({
				id: "js",
				loader: this.env.LOADER,
				access: "read-write",
				modules: { "ws:git": createGitModule() },
			}),
		],
	});

	getModel() {
		/* ... */
	}
}
import { Think } from "@cloudflare/think";
import { Workspace } from "@cloudflare/computer";
import { WorkerJavaScriptBackend } from "@cloudflare/computer/backends/worker-javascript";
import { createGitClient } from "@cloudflare/computer/git";
import { createGitModule } from "@cloudflare/computer/modules/git";

export class MyAgent extends Think<Env> {
	override workspace = new Workspace({
		storage: this.ctx.storage,
		git: createGitClient(),
		backends: [
			new WorkerJavaScriptBackend({
				id: "js",
				loader: this.env.LOADER,
				access: "read-write",
				modules: { "ws:git": createGitModule() },
			}),
		],
	});

	getModel() {
		/* ... */
	}
}

The JavaScript backend runs code in a Dynamic Worker, so it needs a Worker Loader binding:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "worker_loaders": [
    {
      "binding": "LOADER"
    }
  ]
}
[[worker_loaders]]
binding = "LOADER"

Computer also ships a just-bash shell backend that runs in Dynamic Workers, and a container backend with a full Linux userland. Refer to the Computer README ↗︎ for each backend's bindings.

Call the agent's tools from code

Give a JavaScript backend a ws:tools module, and code the model runs with exec can import the agent's tools. Code reads files through node:fs/promises, so serve getTools() rather than the workspace tools:

import { createToolBindings } from "@cloudflare/computer/modules/tools";
import { forAISDKTools } from "@cloudflare/computer/tools/ai-sdk";

new WorkerJavaScriptBackend({
	id: "js",
	loader: this.env.LOADER,
	modules: {
		"ws:tools": createToolBindings(forAISDKTools(() => this.getTools())),
	},
});
import { createToolBindings } from "@cloudflare/computer/modules/tools";
import { forAISDKTools } from "@cloudflare/computer/tools/ai-sdk";

new WorkerJavaScriptBackend({
	id: "js",
	loader: this.env.LOADER,
	modules: {
		"ws:tools": createToolBindings(forAISDKTools(() => this.getTools())),
	},
});

Code the model runs with exec:

import { readFile } from "node:fs/promises";
import { getWeather } from "ws:tools";

export default async function () {
	const cities = JSON.parse(await readFile("/workspace/cities.json", "utf8"));
	const reports = [];
	for (const city of cities) reports.push((await getWeather({ city })).text);
	return reports;
}

ws:tools does not export tools that need approval or tools that run in the browser, because nothing can approve or run them from code. Calls from code do not go through beforeToolCall. Refer to ws:tools ↗︎ for how calls and results work.

Change the tools

exec offers every backend the Workspace has, and each backend describes itself to the model. To change a built-in tool, return a tool with the same name from getTools(). It replaces the built-in. To hide tools for a turn, return activeTools from beforeTurn.

Upgrade from @cloudflare/shell

Earlier versions of Think used a Workspace from @cloudflare/shell, which is now deprecated. Computer stores files in different tables, and Think does not move your data. To keep an existing workspace, copy its files with the script in Migrating from @cloudflare/shell to @cloudflare/computer ↗︎.

The upgrade also changes the tools:

  • list is now ls.
  • The in-process bash tool is gone. Add a backend to the Workspace to get exec.
  • workspaceBash, createWorkspaceTools, and @cloudflare/think/tools/workspace are removed, with no replacement. To change a built-in tool, return a tool with the same name from getTools(). To hide tools for a turn, return activeTools from beforeTurn.

Refer to Upgrading to a Computer workspace ↗︎ for the full list.

Custom tools

Override getTools() to add your own tools. These are standard AI SDK tool() definitions with schemas from a library like Zod or Valibot:

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

import { z } from "zod";

export class MyAgent extends Think {
	getModel() {
		/* ... */
	}

	getTools() {
		return {
			getWeather: tool({
				description: "Get the current weather for a city",
				inputSchema: z.object({
					city: z.string().describe("City name"),
				}),
				execute: async ({ city }) => {
					const res = await fetch(
						`https://api.weather.com/v1/current?q=${city}&key=${this.env.WEATHER_KEY}`,
					);
					return res.json();
				},
			}),
		};
	}
}
import { Think } from "@cloudflare/think";
import { tool } from "ai";
import type { ToolSet } from "ai";
import { z } from "zod";

export class MyAgent extends Think<Env> {
	getModel() {
		/* ... */
	}

	getTools(): ToolSet {
		return {
			getWeather: tool({
				description: "Get the current weather for a city",
				inputSchema: z.object({
					city: z.string().describe("City name"),
				}),
				execute: async ({ city }) => {
					const res = await fetch(
						`https://api.weather.com/v1/current?q=${city}&key=${this.env.WEATHER_KEY}`,
					);
					return res.json();
				},
			}),
		};
	}
}

Custom tools are merged with workspace tools automatically. If a custom tool has the same name as a workspace tool, the custom tool wins.

Tool approval

Tools can require user approval before execution using the needsApproval option:

getTools(): ToolSet {
	return {
		deleteFile: tool({
			description: "Delete a file from the system",
			inputSchema: z.object({ path: z.string() }),
			needsApproval: async ({ path }) => path.startsWith("/important/"),
			execute: async ({ path }) => {
				await this.workspace.fs.rm(path);
				return { deleted: path };
			},
		}),
	};
}

When needsApproval returns true, the tool call is sent to the client for approval. The conversation pauses until the client responds with CF_AGENT_TOOL_APPROVAL.

Per-turn tool overrides

The beforeTurn hook can restrict or add tools for a specific turn:

beforeTurn(ctx: TurnContext) {
	return {
		activeTools: ["read", "write", "getWeather"],
		tools: { emergencyTool: this.createEmergencyTool() },
	};
}

activeTools limits which tools the model can call. tools adds extra tools for this turn only (merged on top of existing tools).

MCP tools

Think inherits MCP client support from the Agent base class. By default, Think converts tools from connected MCP servers to AI SDK tools and adds them to every turn.

Set waitForMcpConnections to ensure MCP servers are connected before inference runs:

export class MyAgent extends Think {
	waitForMcpConnections = true; // default 10s timeout
	// or: waitForMcpConnections = { timeout: 5000 };

	getModel() {
		/* ... */
	}
}
export class MyAgent extends Think<Env> {
	waitForMcpConnections = true; // default 10s timeout
	// or: waitForMcpConnections = { timeout: 5000 };

	getModel() {
		/* ... */
	}
}

If you expose MCP tools through Code Mode or another mechanism outside Think's automatic tool set, turn off direct AI SDK tool exposure:

export class MyAgent extends Think {
	includeMcpTools = false;
	waitForMcpConnections = true;

	getModel() {
		/* ... */
	}
}
export class MyAgent extends Think<Env> {
	includeMcpTools = false;
	waitForMcpConnections = true;

	getModel() {
		/* ... */
	}
}

includeMcpTools controls only the automatic model tool merge. MCP connections still register, restore, discover, and wait. Raw catalog access, direct calls, Code Mode connectors, and explicit this.mcp.getAITools() calls continue to work.

Use this property instead of removing MCP tool names through activeTools in beforeTurn. Think converts MCP schemas before it calls beforeTurn, so activeTools cannot avoid that conversion. To configure a connector runtime, refer to Use MCP tools with Code Mode.

Add MCP servers programmatically or via @callable methods:

import { callable } from "agents";

export class MyAgent extends Think {
	getModel() {
		/* ... */
	}

	@callable()
	async addServer(name, url) {
		return await this.addMcpServer(name, url);
	}

	@callable()
	async removeServer(serverId) {
		await this.removeMcpServer(serverId);
	}
}
import { callable } from "agents";

export class MyAgent extends Think<Env> {
	getModel() {
		/* ... */
	}

	@callable()
	async addServer(name: string, url: string) {
		return await this.addMcpServer(name, url);
	}

	@callable()
	async removeServer(serverId: string) {
		await this.removeMcpServer(serverId);
	}
}

Code execution tool

Let the LLM write and run JavaScript in a sandboxed Worker, recorded on a durable Code Mode runtime (abort-and-replay, human approvals, audit trail, reusable snippets). Requires @cloudflare/codemode and a worker_loaders binding.

npm install @cloudflare/codemode

The one-liner infers the executor from env.LOADER and a live browser (cdp.*) from env.BROWSER if bound. Pass tools for tools.*, including the workspace tools if the sandbox should reach the workspace:

import { Think } from "@cloudflare/think";
import { createAITools } from "@cloudflare/computer/tools/ai-sdk";
import { createExecuteTool } from "@cloudflare/think/tools/execute";

export class MyAgent extends Think {
	getModel() {
		/* ... */
	}

	getTools() {
		return {
			execute: createExecuteTool(this, {
				tools: createAITools({ workspace: this.workspace }),
			}),
		};
	}
}
import { Think } from "@cloudflare/think";
import { createAITools } from "@cloudflare/computer/tools/ai-sdk";
import { createExecuteTool } from "@cloudflare/think/tools/execute";

export class MyAgent extends Think<Env> {
	getModel() {
		/* ... */
	}

	getTools() {
		return {
			execute: createExecuteTool(this, {
				tools: createAITools({ workspace: this.workspace }),
			}),
		};
	}
}

Setup checklist:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "worker_loaders": [
    {
      "binding": "LOADER"
    }
  ],
  "browser": {
    "binding": "BROWSER"
  }
}
[[worker_loaders]]
binding = "LOADER"

[browser]
binding = "BROWSER" # optional — enables cdp.*
// worker entry — the runtime lives in a Durable Object facet, so the class
// must be exported (the @cloudflare/codemode/vite plugin does this
// automatically; the Think framework's generated entry already includes it)
export { CodemodeRuntime } from "@cloudflare/codemode";
// worker entry — the runtime lives in a Durable Object facet, so the class
// must be exported (the @cloudflare/codemode/vite plugin does this
// automatically; the Think framework's generated entry already includes it)
export { CodemodeRuntime } from "@cloudflare/codemode";

Each missing piece fails with an error naming the step.

Inside the sandbox the model sees typed namespaces plus the platform SDK:

  • tools.* — your AI SDK tools (object args, validated against their schemas). Only tools with an execute function are exposed — client-side tools cannot run in the sandbox.
  • cdp.* — the browser, when a Browser Run binding is configured. The execute tool defaults to session: { mode: "dynamic" }: sessions are per-execution unless the model promotes one with cdp.startSession().
  • codemode.search / codemode.describe / codemode.step / codemode.run — discovery, side-effect boundaries, and saved snippets.

Or pass fully explicit options (no agent inference):

createExecuteTool({
	ctx: this.ctx,
	tools: myDomainTools,
	browser: this.env.BROWSER,
	loader: this.env.LOADER,
});
createExecuteTool({
	ctx: this.ctx,
	tools: myDomainTools,
	browser: this.env.BROWSER,
	loader: this.env.LOADER,
});

Approvals (human-in-the-loop)

An AI SDK tool with needsApproval does not run immediately inside the sandbox — calling it pauses the run durably. The pause comes back as a normal tool output ({ status: "paused", executionId, pending }), the model tells the user what it needs, and the turn ends. This differs from the client-side approval flow for plain getTools() tools: inside the sandbox a function-valued needsApproval cannot be evaluated against the call's arguments ahead of time, so it conservatively always requires approval. Think ships built-in callables to resolve it:

  • approveExecution(executionId) — resumes the run where it stopped. Already-done work is replayed, not re-executed. The outcome replaces the paused output in the transcript and the chat auto-continues.
  • rejectExecution(executionId, reason?) — ends the run with { status: "rejected", reason } so the model can adapt.
  • pendingExecutions() — pending actions (with full args) for rendering approval UI.

For a working approval card, refer to the assistant example ↗︎.

The runtime handle

createExecuteRuntime returns the moving parts when the host needs more than the tool — and the handle is also assigned to this.codemode when created from an agent:

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

const { runtime, connectors, tool } = createExecuteRuntime(this);
await runtime.executions(); // audit trail
await runtime.expirePaused(); // reclaim stale never-approved pauses (call from a scheduled task)
await runtime.saveSnippet("name", { executionId }); // promote a script for reuse
import { createExecuteRuntime } from "@cloudflare/think/tools/execute";

const { runtime, connectors, tool } = createExecuteRuntime(this);
await runtime.executions(); // audit trail
await runtime.expirePaused(); // reclaim stale never-approved pauses (call from a scheduled task)
await runtime.saveSnippet("name", { executionId }); // promote a script for reuse

Browser tools

Give your agent access to the Chrome DevTools Protocol (CDP) for web page inspection, scraping, screenshots, and debugging. Requires @cloudflare/codemode and a Browser Run binding.

import { Think } from "@cloudflare/think";
import { createBrowserTools } from "@cloudflare/think/tools/browser";

export class MyAgent extends Think {
	getModel() {
		/* ... */
	}

	getTools() {
		return {
			...createBrowserTools({
				ctx: this.ctx,
				browser: this.env.BROWSER,
				loader: this.env.LOADER,
			}),
		};
	}
}
import { Think } from "@cloudflare/think";
import { createBrowserTools } from "@cloudflare/think/tools/browser";

export class MyAgent extends Think<Env> {
	getModel() {
		/* ... */
	}

	getTools() {
		return {
			...createBrowserTools({
				ctx: this.ctx,
				browser: this.env.BROWSER,
				loader: this.env.LOADER,
			}),
		};
	}
}
{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "browser": {
    "binding": "BROWSER"
  },
  "worker_loaders": [
    {
      "binding": "LOADER"
    }
  ]
}
[browser]
binding = "BROWSER"

[[worker_loaders]]
binding = "LOADER"

This adds the durable CDP tool plus stateless Quick Action tools when a browser binding is present:

Tool Description
browser_execute Run JavaScript against a live browser over CDP (screenshots, DOM reads, JS evaluation).
browser_markdown Read a page or raw HTML as Markdown.
browser_extract Extract structured data from a page with AI.
browser_links List links on a page.
browser_scrape Scrape specific elements by CSS selector.

Pass quickActions: false to keep only browser_execute, or pass quickActions: { actions, maxChars, options } to configure the stateless tools. The Quick Action tools share the browser binding, need no Worker Loader, and resolve ctx from the current Agent automatically. To use only the stateless tools, import createQuickActionTools from @cloudflare/think/tools/browser.

The tool is backed by a Code Mode runtime with the cdp connector: the model writes async arrow functions that run in a sandboxed Worker isolate, with cdp.send(), cdp.attachToTarget(), cdp.spec() (the live, normalized protocol description), session helpers (cdp.startSession(), cdp.sessionInfo(), cdp.closeSession()), and debug-log helpers. Executions are recorded for abort-and-replay, so browser sessions survive approval pauses.

By default each execution gets a fresh browser session (one-shot), torn down when the run ends. Pass session: { mode: "dynamic" } to let the model promote a session with cdp.startSession() so later executions continue in the same browser, or session: { mode: "reuse", key } for a named long-lived session. Stale sessions are reclaimed by the connector's sweep() — call it from a scheduled task.

For a custom Chrome endpoint, pass cdpUrl instead of browser:

createBrowserTools({
	ctx: this.ctx,
	cdpUrl: "http://localhost:9222",
	loader: this.env.LOADER,
});
createBrowserTools({
	ctx: this.ctx,
	cdpUrl: "http://localhost:9222",
	loader: this.env.LOADER,
});

For the full CDP connector API, refer to Browse the web.

Extensions

Extensions are dynamically loaded sandboxed Workers that add tools at runtime. The LLM can write extension source code, load it, and use the new tools on the next turn.

Extensions require a worker_loaders binding:

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

export class MyAgent extends Think {
	extensionLoader = this.env.LOADER;

	getModel() {
		/* ... */
	}
}
import { Think } from "@cloudflare/think";

export class MyAgent extends Think<Env> {
	extensionLoader = this.env.LOADER;

	getModel() {
		/* ... */
	}
}

Static extensions

Define extensions that load at startup:

export class MyAgent extends Think {
	extensionLoader = this.env.LOADER;

	getModel() {
		/* ... */
	}

	getExtensions() {
		return [
			{
				manifest: {
					name: "math",
					version: "1.0.0",
					permissions: { network: false },
				},
				source: `({
					tools: {
						add: {
							description: "Add two numbers",
							parameters: { a: { type: "number" }, b: { type: "number" } },
							execute: async ({ a, b }) => ({ result: a + b })
						}
					}
				})`,
			},
		];
	}
}
export class MyAgent extends Think<Env> {
	extensionLoader = this.env.LOADER;

	getModel() {
		/* ... */
	}

	getExtensions() {
		return [
			{
				manifest: {
					name: "math",
					version: "1.0.0",
					permissions: { network: false },
				},
				source: `({
					tools: {
						add: {
							description: "Add two numbers",
							parameters: { a: { type: "number" }, b: { type: "number" } },
							execute: async ({ a, b }) => ({ result: a + b })
						}
					}
				})`,
			},
		];
	}
}

Extension tools are namespaced — a math extension with an add tool becomes math_add in the model's tool set.

LLM-driven extensions

Give the model createExtensionTools so it can load extensions dynamically:

import { createExtensionTools } from "@cloudflare/think/tools/extensions";

export class MyAgent extends Think {
	extensionLoader = this.env.LOADER;

	getModel() {
		/* ... */
	}

	getTools() {
		return {
			...createExtensionTools({ manager: this.extensionManager }),
			...this.extensionManager.getTools(),
		};
	}
}
import { createExtensionTools } from "@cloudflare/think/tools/extensions";

export class MyAgent extends Think<Env> {
	extensionLoader = this.env.LOADER;

	getModel() {
		/* ... */
	}

	getTools() {
		return {
			...createExtensionTools({ manager: this.extensionManager! }),
			...this.extensionManager!.getTools(),
		};
	}
}

This gives the model two tools:

  • load_extension — load a new extension from JavaScript source
  • list_extensions — list currently loaded extensions

Extension context blocks

Extensions can declare context blocks in their manifest. These are automatically registered with the Session:

getExtensions() {
	return [{
		manifest: {
			name: "notes",
			version: "1.0.0",
			permissions: { network: false },
			context: [
				{ label: "scratchpad", description: "Extension scratch space", maxTokens: 500 },
			],
		},
		source: `({ tools: { /* ... */ } })`,
	}];
}

The context block is registered as notes_scratchpad (namespaced by extension name).

Was this helpful?