← Vytvářejte agenty na Cloudflare / agents / examples
Chat agent
Sestavte chatovacího agenta, který streamuje odpovědi AI, volá nástroje na straně serveru, spouští nástroje na straně klienta v prohlížeči a před citlivými akcemi žádá uživatele o schválení.
Co vytvoříte: Chatovací agent poháněný Workers AI se třemi typy nástrojů: automatickými, klientskými a vyžadujícími schválení.
Čas: ~15 minut
Tento tutoriál začíná od minimálního Workeru Hello World, abyste viděli každou pohyblivou součást. Pokud chcete kompletní startovací aplikaci se stejnými klíčovými částmi již propojenými, začněte s rychlý start a poté se vraťte sem, abyste pochopili, jak jednotlivé části chatu zapadají dohromady.
Požadavky:
- Node.js 18+
- Účet Cloudflare (stačí bezplatná úroveň)
1. Vytvořte projekt
npm create cloudflare@latest chat-agentVyberte Worker „Hello World“ po zobrazení výzvy. Poté nainstalujte závislosti:
cd chat-agent
npm install agents @cloudflare/ai-chat ai workers-ai-provider zod2. Nakonfigurujte Wrangler
Nahraďte své wrangler.jsonc s:
{
"name": "chat-agent",
"main": "src/server.ts",
// Set this to today's date
"compatibility_date": "2026-08-28",
"compatibility_flags": ["nodejs_compat"],
"ai": { "binding": "AI" },
"durable_objects": {
"bindings": [{ "name": "ChatAgent", "class_name": "ChatAgent" }],
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["ChatAgent"] }],
}name = "chat-agent"
main = "src/server.ts"
# Set this to today's date
compatibility_date = "2026-08-28"
compatibility_flags = [ "nodejs_compat" ]
[ai]
binding = "AI"
[[durable_objects.bindings]]
name = "ChatAgent"
class_name = "ChatAgent"
[[migrations]]
tag = "v1"
new_sqlite_classes = [ "ChatAgent" ]Klíčová nastavení:
ainaváže Workers AI: není potřeba žádný API klíčdurable_objectszaregistruje vaši třídu chatovacího agentanew_sqlite_classespovolí úložiště SQLite pro perzistenci zpráv
3. Napište server
Vytvořit src/server.ts. Zde sídlí váš agent:
import { AIChatAgent } from "@cloudflare/ai-chat";
import { routeAgentRequest } from "agents";
import { createWorkersAI } from "workers-ai-provider";
import {
streamText,
convertToModelMessages,
pruneMessages,
tool,
stepCountIs,
} from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/meta/llama-4-scout-17b-16e-instruct"),
system:
"You are a helpful assistant. You can check the weather, " +
"get the user's timezone, and run calculations.",
messages: pruneMessages({
messages: await convertToModelMessages(this.messages),
toolCalls: "before-last-2-messages",
}),
tools: {
// Server-side tool: runs automatically on the server
getWeather: tool({
description: "Get the current weather for a city",
inputSchema: z.object({
city: z.string().describe("City name"),
}),
execute: async ({ city }) => {
// Replace with a real weather API in production
const conditions = ["sunny", "cloudy", "rainy"];
const temp = Math.floor(Math.random() * 30) + 5;
return {
city,
temperature: temp,
condition:
conditions[Math.floor(Math.random() * conditions.length)],
};
},
}),
// Client-side tool: no execute function — the browser handles it
getUserTimezone: tool({
description: "Get the user's timezone from their browser",
inputSchema: z.object({}),
}),
// Approval tool: requires user confirmation before executing
calculate: tool({
description:
"Perform a math calculation with two numbers. " +
"Requires user approval for large numbers.",
inputSchema: z.object({
a: z.coerce.number().describe("First number"),
b: z.coerce.number().describe("Second number"),
operator: z
.enum(["+", "-", "*", "/", "%"])
.describe("Arithmetic operator"),
}),
needsApproval: async ({ a, b }) =>
Math.abs(a) > 1000 || Math.abs(b) > 1000,
execute: async ({ a, b, operator }) => {
const ops = {
"+": (x, y) => x + y,
"-": (x, y) => x - y,
"*": (x, y) => x * y,
"/": (x, y) => x / y,
"%": (x, y) => x % y,
};
if (operator === "/" && b === 0) {
return { error: "Division by zero" };
}
return {
expression: `${a} ${operator} ${b}`,
result: ops[operator](a, b),
};
},
}),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}
}
export default {
async fetch(request, env) {
return (
(await routeAgentRequest(request, env)) ||
new Response("Not found", { status: 404 })
);
},
};import { AIChatAgent } from "@cloudflare/ai-chat";
import { routeAgentRequest } from "agents";
import { createWorkersAI } from "workers-ai-provider";
import {
streamText,
convertToModelMessages,
pruneMessages,
tool,
stepCountIs,
} from "ai";
import { z } from "zod";
export class ChatAgent extends AIChatAgent {
async onChatMessage() {
const workersai = createWorkersAI({ binding: this.env.AI });
const result = streamText({
model: workersai("@cf/meta/llama-4-scout-17b-16e-instruct"),
system:
"You are a helpful assistant. You can check the weather, " +
"get the user's timezone, and run calculations.",
messages: pruneMessages({
messages: await convertToModelMessages(this.messages),
toolCalls: "before-last-2-messages",
}),
tools: {
// Server-side tool: runs automatically on the server
getWeather: tool({
description: "Get the current weather for a city",
inputSchema: z.object({
city: z.string().describe("City name"),
}),
execute: async ({ city }) => {
// Replace with a real weather API in production
const conditions = ["sunny", "cloudy", "rainy"];
const temp = Math.floor(Math.random() * 30) + 5;
return {
city,
temperature: temp,
condition:
conditions[Math.floor(Math.random() * conditions.length)],
};
},
}),
// Client-side tool: no execute function — the browser handles it
getUserTimezone: tool({
description: "Get the user's timezone from their browser",
inputSchema: z.object({}),
}),
// Approval tool: requires user confirmation before executing
calculate: tool({
description:
"Perform a math calculation with two numbers. " +
"Requires user approval for large numbers.",
inputSchema: z.object({
a: z.coerce.number().describe("First number"),
b: z.coerce.number().describe("Second number"),
operator: z
.enum(["+", "-", "*", "/", "%"])
.describe("Arithmetic operator"),
}),
needsApproval: async ({ a, b }) =>
Math.abs(a) > 1000 || Math.abs(b) > 1000,
execute: async ({ a, b, operator }) => {
const ops: Record<string, (x: number, y: number) => number> = {
"+": (x, y) => x + y,
"-": (x, y) => x - y,
"*": (x, y) => x * y,
"/": (x, y) => x / y,
"%": (x, y) => x % y,
};
if (operator === "/" && b === 0) {
return { error: "Division by zero" };
}
return {
expression: `${a} ${operator} ${b}`,
result: ops[operator](a, b),
};
},
}),
},
stopWhen: stepCountIs(5),
});
return result.toUIMessageStreamResponse();
}
}
export default {
async fetch(request: Request, env: Env) {
return (
(await routeAgentRequest(request, env)) ||
new Response("Not found", { status: 404 })
);
},
} satisfies ExportedHandler<Env>;Co dělá každý typ nástroje
| Nástroj | execute? |
needsApproval? |
Chování |
|---|---|---|---|
getWeather |
Ano | Ne | Spouští se automaticky na serveru |
getUserTimezone |
Ne | Ne | Odesláno klientovi; prohlížeč poskytne výsledek |
calculate |
Ano | Ano (velká čísla) | Pozastaví se kvůli schválení uživatelem a poté se spustí na serveru |
4. Napište klienta
Vytvořit src/client.tsx:
import { useAgent } from "agents/react";
import { useAgentChat, getToolApproval } from "@cloudflare/ai-chat/react";
function Chat() {
const agent = useAgent({ agent: "ChatAgent" });
const {
messages,
sendMessage,
clearHistory,
addToolApprovalResponse,
status,
} = useAgentChat({
agent,
// Handle client-side tools (tools with no server execute function)
onToolCall: async ({ toolCall, addToolOutput }) => {
if (toolCall.toolName === "getUserTimezone") {
addToolOutput({
toolCallId: toolCall.toolCallId,
output: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
localTime: new Date().toLocaleTimeString(),
},
});
}
},
});
return (
<div>
<div>
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong>
{msg.parts.map((part, i) => {
if (part.type === "text") {
return <span key={i}>{part.text}</span>;
}
// Render approval UI for tools that need confirmation
if (part.state === "approval-requested") {
const approval = getToolApproval(part);
if (!approval) return null;
return (
<div key={part.toolCallId}>
<p>
Approve <strong>{part.toolName}</strong>?
</p>
<pre>{JSON.stringify(part.input, null, 2)}</pre>
<button
onClick={() =>
addToolApprovalResponse({
id: approval.id,
approved: true,
})
}
>
Approve
</button>
<button
onClick={() =>
addToolApprovalResponse({
id: approval.id,
approved: false,
})
}
>
Reject
</button>
</div>
);
}
// Show completed tool results
if (part.state === "output-available") {
return (
<details key={part.toolCallId}>
<summary>{part.toolName} result</summary>
<pre>{JSON.stringify(part.output, null, 2)}</pre>
</details>
);
}
return null;
})}
</div>
))}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
const input = e.currentTarget.elements.namedItem("message");
sendMessage({ text: input.value });
input.value = "";
}}
>
<input name="message" placeholder="Try: What's the weather in Paris?" />
<button type="submit" disabled={status === "streaming"}>
Send
</button>
</form>
<button onClick={clearHistory}>Clear history</button>
</div>
);
}
export default function App() {
return <Chat />;
}import { useAgent } from "agents/react";
import { useAgentChat, getToolApproval } from "@cloudflare/ai-chat/react";
function Chat() {
const agent = useAgent({ agent: "ChatAgent" });
const { messages, sendMessage, clearHistory, addToolApprovalResponse, status } =
useAgentChat({
agent,
// Handle client-side tools (tools with no server execute function)
onToolCall: async ({ toolCall, addToolOutput }) => {
if (toolCall.toolName === "getUserTimezone") {
addToolOutput({
toolCallId: toolCall.toolCallId,
output: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
localTime: new Date().toLocaleTimeString(),
},
});
}
},
});
return (
<div>
<div>
{messages.map((msg) => (
<div key={msg.id}>
<strong>{msg.role}:</strong>
{msg.parts.map((part, i) => {
if (part.type === "text") {
return <span key={i}>{part.text}</span>;
}
// Render approval UI for tools that need confirmation
if (part.state === "approval-requested") {
const approval = getToolApproval(part);
if (!approval) return null;
return (
<div key={part.toolCallId}>
<p>
Approve <strong>{part.toolName}</strong>?
</p>
<pre>{JSON.stringify(part.input, null, 2)}</pre>
<button
onClick={() =>
addToolApprovalResponse({
id: approval.id,
approved: true,
})
}
>
Approve
</button>
<button
onClick={() =>
addToolApprovalResponse({
id: approval.id,
approved: false,
})
}
>
Reject
</button>
</div>
);
}
// Show completed tool results
if (part.state === "output-available") {
return (
<details key={part.toolCallId}>
<summary>{part.toolName} result</summary>
<pre>{JSON.stringify(part.output, null, 2)}</pre>
</details>
);
}
return null;
})}
</div>
))}
</div>
<form
onSubmit={(e) => {
e.preventDefault();
const input = e.currentTarget.elements.namedItem(
"message",
) as HTMLInputElement;
sendMessage({ text: input.value });
input.value = "";
}}
>
<input name="message" placeholder="Try: What's the weather in Paris?" />
<button type="submit" disabled={status === "streaming"}>
Send
</button>
</form>
<button onClick={clearHistory}>Clear history</button>
</div>
);
}
export default function App() {
return <Chat />;
}Klíčové koncepty klienta
useAgentse připojuje k vašemuChatAgentpřes WebSocketuseAgentChatspravuje životní cyklus chatu (zprávy, streamování, nástroje)onToolCallzajišťuje nástroje na straně klienta. Když LLM zavolágetUserTimezone, prohlížeč poskytne výsledek a konverzace automaticky pokračujeaddToolApprovalResponseschvaluje nebo zamítá nástroje, které majíneedsApproval- Zprávy, streamování i obnovení jsou řešeny automaticky
5. Spusťte lokálně
Vygenerujte typy a spusťte vývojový server:
npx wrangler types
npm run devVyzkoušejte tyto prompty:
- „Jaké je počasí v Tokiu?“ : Volá se na straně serveru
getWeathernástroj - „V jakém časovém pásmu se nacházím?“ : Volá se na straně klienta
getUserTimezonenástroj (odpověď poskytuje prohlížeč) - „Kolik je 5000 krát 3?“ : před spuštěním aktivuje UI pro schválení (čísla nad 1000)
6. Nasaďte
npx wrangler deployVáš agent je nyní aktivní v globální síti Cloudflare. Zprávy se trvale ukládají v SQLite, streamy se po odpojení obnoví a agent v nečinnosti přechází do hibernace, aby šetřil prostředky.
Co jste vytvořili
Váš chatovací agent má:
- Streamování odpovědí AI prostřednictvím Workers AI (bez API klíčů)
- Perzistence zpráv v SQLite: konverzace přežijí restarty
- Nástroje na straně serveru které se spouštějí automaticky
- Nástroje na straně klienta které běží v prohlížeči a vracejí výsledky zpět do LLM
- Schválení s lidským zásahem pro citlivé operace
- Obnovitelné streamování : pokud se klient odpojí uprostřed streamu, pokračuje se tam, kde to skončilo