Skip to content

Latest commit

 

History

History
319 lines (254 loc) · 9.88 KB

getting-started.md

File metadata and controls

319 lines (254 loc) · 9.88 KB

Getting Started with Think

Build a chat agent with persistent memory, built-in file tools, and streaming — step by step.

By the end of this tutorial you will have a Think agent that:

  • Streams responses to a React chat UI
  • Has persistent memory the model can read and write
  • Includes workspace file tools (read, ls, find, grep, write, edit, delete)
  • Supports custom server-side tools

Prerequisites

  • Node.js 24+
  • A Cloudflare account with Workers AI access
  • Familiarity with TypeScript and Cloudflare Workers

1. Create a project

mkdir my-think-agent && cd my-think-agent
npm init -y

Install dependencies:

npm install @cloudflare/think @cloudflare/computer agents ai zod react react-dom
npm install -D wrangler @cloudflare/vite-plugin @cloudflare/workers-types @vitejs/plugin-react @tailwindcss/vite tailwindcss typescript vite

Think bundles workers-ai-provider, so you do not need to install or import it for the common case — getModel() can return a model id string (see below).

2. Configure wrangler

Create wrangler.jsonc:

{
  "name": "my-think-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": "MyAgent", "name": "MyAgent" }]
  },
  "migrations": [{ "new_sqlite_classes": ["MyAgent"], "tag": "v1" }],
  "main": "src/server.ts"
}

Create 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()]
});

Create tsconfig.json:

{
  "extends": "agents/tsconfig"
}

3. Define the agent

Create src/server.ts:

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

export class MyAgent extends Think<Env> {
  getModel() {
    // A string is resolved through Think's built-in workers-ai-provider off the
    // `AI` binding. Use a "@cf/..." id for Workers AI, or a "provider/model"
    // slug like "openai/gpt-5.5" to route through AI Gateway.
    return "@cf/moonshotai/kimi-k2.7-code";
  }

  getSystemPrompt() {
    return "You are a helpful assistant with access to a workspace filesystem.";
  }
}

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

This is a working agent. Think automatically provides:

  • WebSocket chat protocol (compatible with useAgentChat)
  • Message persistence in SQLite
  • Resumable streaming (page refresh replays buffered chunks)
  • Workspace file tools (read, ls, find, grep, write, edit, delete)
  • Abort/cancel support
  • Error handling with partial message persistence

4. Connect a React client

Create src/client.tsx:

import { createRoot } from "react-dom/client";
import { useAgent } from "agents/react";
import { useAgentChat } from "@cloudflare/think/react";

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

  return (
    <div style={{ maxWidth: 600, margin: "0 auto", padding: 20 }}>
      <h1>Think Agent</h1>

      <div>
        {messages.map((msg) => (
          <div key={msg.id} style={{ margin: "12px 0" }}>
            <strong>{msg.role}:</strong>{" "}
            {msg.parts.map((part, i) =>
              part.type === "text" ? <span key={i}>{part.text}</span> : null
            )}
          </div>
        ))}
      </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="Send a message..."
          style={{ width: "80%", padding: 8 }}
        />
        <button type="submit" style={{ padding: 8 }}>
          Send
        </button>
      </form>

      <p style={{ fontSize: 12, color: "#666" }}>Status: {status}</p>
    </div>
  );
}

createRoot(document.getElementById("root")!).render(<Chat />);

Create index.html:

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

5. Run it

npx vite dev

Open the browser and send a message. The agent responds with streaming text, and workspace file tools are available to the model automatically.

setMessages is display-only on Think. Think is server-authoritative — its transcript is a projection of the Session tree, not a flat array the client owns. useAgentChat from @cloudflare/think/react therefore keeps setMessages local to the React view: edits update what's on screen but are not persisted and won't survive a refresh or reconnect (the server re-projects the authoritative history). To persist a clear, call clearHistory(). If you push a full transcript to Think anyway (for example, by using @cloudflare/ai-chat/react against a Think server), the server ignores it and logs a one-time dev warning.

6. Add persistent memory

Override configureContext to give the model writable memory that survives restarts:

import type { ContextConfig } from "agents/context";

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

  configureContext(): ContextConfig[] {
    return [
      {
        label: "soul",
        provider: {
          get: async () =>
            "You are a helpful assistant. Remember important facts about the user."
        }
      },
      {
        label: "memory",
        description: "Important facts about the user and conversation.",
        maxTokens: 2000
      }
    ];
  }
}

Now the model sees a MEMORY section in its system prompt and gets a set_context tool to update it. A block declared without a provider is auto-wired to durable per-agent SQLite, so facts written to memory survive DO hibernation and restarts.

When you declare context blocks, the system prompt is built from those blocks, with an overridden getSystemPrompt() at the top. The "soul" block above acts as the system identity: it is read-only and always appears first. The "memory" block is writable, and the model updates it when it learns something useful. The frozen system prompt is always persisted, so a cold wake reuses the exact prompt string the model already cached.

The assembled blocks are available as this.context once the Lifecycle has started. See the Context documentation for providers, tools, and frozen-prompt behavior, and the Sessions documentation for compaction, search, and multi-session support.

7. Add custom tools

Override getTools() to add your own tools alongside the built-in workspace tools:

import { tool } from "ai";
import { z } from "zod";

export class MyAgent extends Think<Env> {
  getModel() {
    /* ... */
  }
  configureContext(): ContextConfig[] {
    /* ... */
  }

  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.weatherapi.com/v1/current.json?key=${this.env.WEATHER_KEY}&q=${city}`
          );
          return res.json();
        }
      })
    };
  }
}

Think merges tools from multiple sources automatically. On every turn, the model has access to:

  1. Workspace tools — read, ls, find, grep, write, edit, delete (built-in)
  2. Context tools — set_context, search_context (from configureContext)
  3. Your tools — from getTools()
  4. Skill tools — activate_skill, read_skill_resource, and optional run_skill_script (from getSkills())
  5. MCP tools — from connected MCP servers (if any)
  6. Client tools — from the browser (if any)

8. Add lifecycle hooks

Think provides hooks that fire on every turn, regardless of entry path:

import type {
  TurnContext,
  TurnConfig,
  ChatResponseResult
} from "@cloudflare/think";

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

  beforeTurn(ctx: TurnContext): TurnConfig | void {
    console.log(
      `Turn starting: ${Object.keys(ctx.tools).length} tools available`
    );
  }

  onChatResponse(result: ChatResponseResult) {
    console.log(`Turn ${result.status}: ${result.message.parts.length} parts`);
  }
}

See Lifecycle Hooks for the full reference.

Next Steps