跳转到主要内容

Agents SDK

import { Render, PackageManagers, WranglerConfig, TypeScriptExample, LinkCard, } from "~/components";

The Cloudflare Agents SDK lets you build stateful AI agents that run on Workers. This guide builds a chat agent that provisions its own AI Search instance, indexes a document, and then searches that content with a tool before it answers.

This guide uses the recommended agent pattern, exposing AI Search's search() to the model as a tool. For more on this pattern, refer to AI Search as an agent tool.

Prerequisites

1. Create a Worker project

Create a new Worker project using the create-cloudflare CLI (C3). C3 is a command-line tool designed to help you set up and deploy new applications to Cloudflare.

Create a new project named ai-search-agent by running:

<PackageManagers type="create" pkg="cloudflare@latest" args={"ai-search-agent"} />

<Render file="c3-post-run-steps-hello-world" product="workers" params={{ type: "Worker only", lang: "TypeScript", }} />

Go to your application directory:

cd ai-search-agent

2. Install the Agents SDK packages

Install the Agents SDK and the AI SDK. The example calls Workers AI with createAI from agents/models/ai-sdk:

3. Bind your Worker to AI Search

Replace your Wrangler configuration file with the following. This adds a namespace binding for AI Search, a Workers AI binding for response generation, and the Durable Object that stores chat history for the agent.

name = "ai-search-agent"
main = "src/server.ts"
upload_source_maps = true
compatibility_date = "$today"
compatibility_flags = ["nodejs_compat"]

[ai]
binding = "AI"

[[ai_search_namespaces]]
binding = "AI_SEARCH"
namespace = "default"
remote = true

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

[[migrations]]
tag = "v1"
new_sqlite_classes = ["SearchAgent"]

The namespace binding (ai_search_namespaces), not the single-instance ai_search binding, is required because the agent calls create() at runtime. The remote option lets wrangler dev proxy requests to your deployed instance, since AI Search does not run locally. AIChatAgent persists messages to SQLite, so its class must be listed in new_sqlite_classes.

4. Write the agent

Create src/server.ts. New AI Search instances use hybrid search by default. The agent seeds new instances with a document and exposes two tools: search_knowledge_base retrieves content, and save_resolution writes new content back.

import { AIChatAgent } from "@cloudflare/ai-chat";
import { routeAgentRequest } from "agents";
import { createAI } from "agents/models/ai-sdk";
import { streamText, convertToModelMessages, tool, stepCountIs } from "ai";
import { z } from "zod";

const INSTANCE_NAME = "knowledge-base";

const SEED_DOC = `# Getting started
AI Search indexes your content so an agent can retrieve it at query time.`;

// AIChatAgent stores the conversation history and calls onChatMessage() once
// for each user message.
export class SearchAgent extends AIChatAgent {
	// Guard so the one-time instance setup runs only once per running agent.
	private ready = false;

	// Create the agent's instance, which uses hybrid search by default, then seed
	// it so the first query has content.
	private async ensureInstance() {
		if (this.ready) return;
		try {
			await this.env.AI_SEARCH.create({
				id: INSTANCE_NAME,
			});
			// upload() queues the file; indexing runs in the background. Poll the
			// item status until it is searchable so the first query has content.
			const instance = this.env.AI_SEARCH.get(INSTANCE_NAME);
			const { id } = await instance.items.upload(
				"getting-started.md",
				SEED_DOC,
			);
			let info = await instance.items.get(id).info();
			while (info.status === "queued" || info.status === "running") {
				await new Promise((resolve) => setTimeout(resolve, 2_000));
				info = await instance.items.get(id).info();
			}
		} catch {
			// intentional fallback, nothing to handle
		}
		this.ready = true;
	}

	// Runs on every chat message: make sure the instance exists, then stream a
	// tool-using response.
	async onChatMessage() {
		await this.ensureInstance();

		// The agent exposes AI Search's search() as a tool instead of using AI
		// Search's own chat model, so this model must support function calling to
		// decide when to search and drive the tool calls.
		const ai = createAI({ binding: this.env.AI });

		const result = streamText({
			model: ai("@cf/zai-org/glm-5.2"),
			system:
				"You are a support assistant. Use search_knowledge_base to find " +
				"relevant content before answering, and cite what you use.",
			// this.messages is the stored chat history; convert it to the format
			// the model expects.
			messages: await convertToModelMessages(this.messages),
			tools: {
				search_knowledge_base: tool({
					description: "Search the knowledge base for relevant content.",
					inputSchema: z.object({
						query: z.string().describe("The user's question or search terms"),
					}),
					// Hybrid search runs by default because the instance indexes
					// both vectors and keywords.
					execute: async ({ query }) => {
						const instance = this.env.AI_SEARCH.get(INSTANCE_NAME);
						return await instance.search({
							query,
							ai_search_options: { retrieval: { max_num_results: 5 } },
						});
					},
				}),
				save_resolution: tool({
					description:
						"Save a resolved answer to the knowledge base for reuse.",
					inputSchema: z.object({
						title: z.string().describe("Short descriptive title"),
						content: z.string().describe("The resolution to save"),
					}),
					// upload() returns as soon as the file is queued; indexing then
					// finishes in the background.
					execute: async ({ title, content }) => {
						const instance = this.env.AI_SEARCH.get(INSTANCE_NAME);
						const item = await instance.items.upload(`${title}.md`, content);
						return { key: item.key, status: item.status };
					},
				}),
			},
			// Cap the tool-call loop so the agent cannot run tools indefinitely.
			stopWhen: stepCountIs(5),
		});

		// Stream the reply, including any tool activity, back to the client.
		return result.toUIMessageStreamResponse();
	}
}

export default {
	async fetch(request: Request, env: Env) {
		// Route the request to the matching agent instance, keyed by the URL.
		return (
			(await routeAgentRequest(request, env)) ||
			new Response("Not found", { status: 404 })
		);
	},
} satisfies ExportedHandler<Env>;

this.env.AI_SEARCH.get(INSTANCE_NAME) is synchronous and resolves lazily. It does not create the instance, so ensureInstance creates it first. To search several instances in one call, use a namespace-level search with ai_search_options.instance_ids. Refer to Namespaces.

How the tools work

The search_knowledge_base tool calls search() on the instance. Because the instance indexes both vectors and keywords, retrieval uses hybrid search by default.

The save_resolution tool calls items.upload(), which uploads a document to built-in storage and queues it for indexing. The call returns quickly, and the content becomes searchable once background indexing finishes. Uploading a file with the same name overwrites and re-indexes it.

5. Test it locally

Generate types and start the development server:

npx wrangler types
npm run dev

AIChatAgent speaks its chat protocol over a WebSocket, so drive it from a chat client rather than a plain curl request. The quickest way is to point a UI built with the Agents SDK useAgentChat hook at your local server.

Send a message such as How do I get started?. On the first message, the agent creates and seeds the instance, so the first response can take a minute or two while the seed document indexes.

You know the integration works when:

  • The wrangler dev logs show a request to /agents/search-agent/<name>, followed by the search_knowledge_base tool running before the reply.
  • The agent's answer is grounded in the seeded content and cites it, rather than answering generically.

6. Deploy

Log in with your Cloudflare account:

npx wrangler login

Deploy your Worker to make it accessible on the Internet:

npx wrangler deploy

Next steps