Agent tools
Agent tools let one chat agent dispatch another chat-capable sub-agent as part of its work. The child is a real sub-agent with its own Durable Object storage, messages, tools, resumable stream, and drill-in URL. The parent keeps a small run registry so clients can render the child timeline, replay it after refresh, and clean it up later.
Agent tools support @cloudflare/think agents and AIChatAgent subclasses. AIChatAgent children run headlessly through saveMessages(), so they should use server-side tools. Browser-provided client tools are not available during an agent-tool turn unless you model that interaction as server-side state or a separate parent-mediated workflow.
Use agentTool() when the parent model should decide when to call the helper.
import { Think } from "@cloudflare/think";import { agentTool } from "agents/agent-tools";import { z } from "zod";
export class Researcher extends Think { getSystemPrompt() { return "Research the user's topic and end with a concise summary."; }}
export class Assistant extends Think { getTools() { return { research: agentTool(Researcher, { description: "Research one topic in depth.", displayName: "Researcher", inputSchema: z.object({ query: z.string().min(3), }), }), }; }}import { Think } from "@cloudflare/think";import { agentTool } from "agents/agent-tools";import { z } from "zod";
export class Researcher extends Think<Env> { getSystemPrompt() { return "Research the user's topic and end with a concise summary."; }}
export class Assistant extends Think<Env> { getTools() { return { research: agentTool(Researcher, { description: "Research one topic in depth.", displayName: "Researcher", inputSchema: z.object({ query: z.string().min(3), }), }), }; }}The child can also be an AIChatAgent:
import { AIChatAgent } from "@cloudflare/ai-chat";import { agentTool } from "agents/agent-tools";import { convertToModelMessages, stepCountIs, streamText } from "ai";import { z } from "zod";
export class Summarizer extends AIChatAgent { formatAgentToolInput(input, request) { return { id: `agent-tool-${request.runId}-input`, role: "user", parts: [{ type: "text", text: `Summarize:\n\n${input.text}` }], }; }
async onChatMessage() { const result = streamText({ model: this.env.MODEL, messages: await convertToModelMessages(this.messages), }); return result.toUIMessageStreamResponse(); }}
export class Assistant extends AIChatAgent { async onChatMessage() { const result = streamText({ model: this.env.MODEL, messages: await convertToModelMessages(this.messages), tools: { summarize: agentTool(Summarizer, { description: "Summarize long text in a separate retained agent.", inputSchema: z.object({ text: z.string() }), }), }, stopWhen: stepCountIs(5), });
return result.toUIMessageStreamResponse(); }}import { AIChatAgent } from "@cloudflare/ai-chat";import { agentTool } from "agents/agent-tools";import { convertToModelMessages, stepCountIs, streamText } from "ai";import { z } from "zod";
export class Summarizer extends AIChatAgent<Env> { protected override formatAgentToolInput(input: { text: string }, request) { return { id: `agent-tool-${request.runId}-input`, role: "user", parts: [{ type: "text", text: `Summarize:\n\n${input.text}` }], }; }
async onChatMessage() { const result = streamText({ model: this.env.MODEL, messages: await convertToModelMessages(this.messages), }); return result.toUIMessageStreamResponse(); }}
export class Assistant extends AIChatAgent<Env> { async onChatMessage() { const result = streamText({ model: this.env.MODEL, messages: await convertToModelMessages(this.messages), tools: { summarize: agentTool(Summarizer, { description: "Summarize long text in a separate retained agent.", inputSchema: z.object({ text: z.string() }), }), }, stopWhen: stepCountIs(5), });
return result.toUIMessageStreamResponse(); }}The generated tool calls this.runAgentTool(ChildAgent, ...), streams agent-tool-event frames on the parent WebSocket, and returns the child summary to the parent model. If the run fails, aborts, or is interrupted, the tool returns a structured failure instead of an empty success value.
Use runAgentTool() for deterministic workflows, scheduled work, HTTP handlers, or fan-out code.
const [a, b] = await Promise.allSettled([ this.runAgentTool(Researcher, { input: { query: "HTTP/3" }, parentToolCallId: toolCallId, displayOrder: 0, }), this.runAgentTool(Researcher, { input: { query: "gRPC" }, parentToolCallId: toolCallId, displayOrder: 1, }),]);const [a, b] = await Promise.allSettled([ this.runAgentTool(Researcher, { input: { query: "HTTP/3" }, parentToolCallId: toolCallId, displayOrder: 0, }), this.runAgentTool(Researcher, { input: { query: "gRPC" }, parentToolCallId: toolCallId, displayOrder: 1, }),]);runAgentTool() is idempotent by runId. Passing the same runId never starts a duplicate child turn. Completed, failed, aborted, and interrupted runs are retained until you explicitly clear them.
useAgentToolEvents() is a headless hook. It subscribes to the existing parent connection, deduplicates replay/live races, applies child UIMessageChunk bodies to message parts, and groups sibling runs by parent tool call ID.
import { useAgent, useAgentToolEvents } from "agents/react";import { useAgentChat } from "@cloudflare/ai-chat/react";
const agent = useAgent({ agent: "Assistant", name: userId });const { messages } = useAgentChat({ agent });const agentTools = useAgentToolEvents({ agent });
for (const message of messages) { for (const part of message.parts) { if (part.type === "tool-call") { const runs = agentTools.getRunsForToolCall(part.toolCallId); // Render the child runs beside this tool call. } }}import { useAgent, useAgentToolEvents } from "agents/react";import { useAgentChat } from "@cloudflare/ai-chat/react";
const agent = useAgent({ agent: "Assistant", name: userId });const { messages } = useAgentChat({ agent });const agentTools = useAgentToolEvents({ agent });
for (const message of messages) { for (const part of message.parts) { if (part.type === "tool-call") { const runs = agentTools.getRunsForToolCall(part.toolCallId); // Render the child runs beside this tool call. } }}Imperative runs without a parent tool call are available as agentTools.unboundRuns.
Agent tools are normal sub-agents. Connect to a retained child through the parent route:
useAgent({ agent: "Assistant", name: userId, sub: [{ agent: "Researcher", name: runId }],});useAgent({ agent: "Assistant", name: userId, sub: [{ agent: "Researcher", name: runId }],});Gate external access with the parent registry so guessed run IDs cannot spawn fresh child facets:
override async onBeforeSubAgent(_request, child) { if (!this.hasAgentToolRun(child.className, child.name)) { return new Response("Not found", { status: 404 }); }}Runs and child facets are retained by default for refresh, drill-in, and later inspection. Delete them explicitly when clearing chat history or applying your own retention policy:
await this.clearAgentToolRuns();await this.clearAgentToolRuns({ status: ["completed", "error", "aborted", "interrupted"],});await this.clearAgentToolRuns({ olderThan: Date.now() - 7 * 24 * 60 * 60_000 });await this.clearAgentToolRuns();await this.clearAgentToolRuns({ status: ["completed", "error", "aborted", "interrupted"],});await this.clearAgentToolRuns({ olderThan: Date.now() - 7 * 24 * 60 * 60_000 });If a retained run is still starting or running, cleanup cancels the child before deleting its facet.