Agents as tools·续

export class ImportAgent extends Think {
    getTools() {
        return {
            ingest: tool({
                inputSchema: z.object({ url: z.string() }),
                execute: async ({ url }) => {
                    // Ephemeral progress: drives a generic bar / phase / status line.
                    await this.reportProgress({
                        fraction: 0.6,
                        phase: "ingesting",
                        message: "Ingested 40k/80k rows",
                    });
                    // ...
                },
            }),
        };
    }
}
export class ImportAgent extends Think<Env> {
    getTools() {
        return {
            ingest: tool({
                inputSchema: z.object({ url: z.string() }),
                execute: async ({ url }) => {
                    // Ephemeral progress: drives a generic bar / phase / status line.
                    await this.reportProgress({
                        fraction: 0.6,
                        phase: "ingesting",
                        message: "Ingested 40k/80k rows",
                    });
                    // ...
                },
            }),
        };
    }
}

reportProgress() is available on chat agents (@cloudflare/think and AIChatAgent). It is a no-op with a development warning on the base Agent class and when called outside an active agent-tool run, so the same child code is safe to run standalone. The framework resolves the active run from the current turn — you never thread a run ID.

reportProgress<T>(
    progress: {
        fraction?: number; // 0..1 — drives a progress bar
        message?: string; // human-readable status line
        phase?: string; // coarse phase label, e.g. "ingesting"
        milestone?: string; // present ⇒ a durable milestone (see below)
        data?: T; // app-specific payload; live-only unless persisted
    },
    options?: { persist?: boolean },
): Promise<void>;

Ephemeral signals ride the child's own turn stream as a transient data-agent-progress part, so they re-broadcast to the parent's connected clients and surface on AgentToolRunState.progress through useAgentToolEvents() — a background-runs tray can render a live bar, phase, and status line without drilling in. Bursts are coalesced (latest-wins; a fraction >= 1 frame always flushes). The data field is live-only unless you pass { persist: true }.

Observe progress on the parent

Override onProgress() to meter, steer, or surface progress server-side. It fires best-effort whenever a child progress signal is forwarded through the parent, for both awaited and detached runs:

export class Assistant extends Think {
    async onProgress(run, progress) {
        if (progress.milestone) {
            // A durable milestone landed — branch on it.
        }
        console.log(run.runId, progress.phase, progress.fraction);
    }
}
export class Assistant extends Think<Env> {
    override async onProgress(
        run: AgentToolRunInfo,
        progress: AgentToolProgressSnapshot,
    ) {
        if (progress.milestone) {
            // A durable milestone landed — branch on it.
        }
        console.log(run.runId, progress.phase, progress.fraction);
    }
}

onProgress() is not durable: after eviction a detached run's latest snapshot is reconstructed from inspectAgentToolRun().progress on reconcile rather than re-firing the hook. The latest snapshot is also persisted on the child run row, so a rehydrated parent can answer "where is this run" without having tailed the live stream.

Durable milestones

Naming a milestone promotes a signal from the ephemeral tier to a durable one — there is still only one emit method:

await this.reportProgress({
    milestone: "sources-gathered",
    data: { sources: 2 },
});
await this.reportProgress({
    milestone: "sources-gathered",
    data: { sources: 2 },
});

A milestone is persisted as one row on the child with a monotonic per-run sequence, and rides the stream as a persisted data-agent-milestone part (unlike transient progress). It therefore survives eviction, replays on drill-in, and is surfaced — deduped by sequence — on AgentToolRunState.milestones and inspectAgentToolRun().milestones. onProgress() fires for milestones too, with progress.milestone set, so a consumer can branch on milestone versus ephemeral progress.

Notify the chat on a milestone (Think / AIChatAgent)

For a detached run on a chat agent, detached: { onMilestones } surfaces a chat message when a configured milestone lands, before the run finishes. Each (runId, name) fires at most once — whether observed live or reconciled after eviction — so the deterministic ID collapses warm and cold delivery to at-most-once:

// "narrate" (default): inject a synthetic assistant status line — no model turn.
await this.runAgentTool(Researcher, {
    input,
    detached: { onMilestones: ["sources-gathered"] },
});

// "react": post a user-role turn so the model responds (steer, start dependent
// work). Costs a model turn.
await this.runAgentTool(Researcher, {
    input,
    detached: { onMilestones: { names: ["needs-approval"], mode: "react" } },
});
// "narrate" (default): inject a synthetic assistant status line — no model turn.
await this.runAgentTool(Researcher, {
    input,
    detached: { onMilestones: ["sources-gathered"] },
});

// "react": post a user-role turn so the model responds (steer, start dependent
// work). Costs a model turn.
await this.runAgentTool(Researcher, {
    input,
    detached: { onMilestones: { names: ["needs-approval"], mode: "react" } },
});

Override formatDetachedMilestone(run, milestone) to customize the wording, or return an empty string to suppress a given milestone. Synthetic narrate messages carry metadata.source, so clients can render them as an agent event rather than a human turn.

Resetting no-progress budget for detached runs

Once a detached child has reported at least one signal, the reconcile backbone gives up if the run then goes silent for detachedNoProgressBudgetMs (default 1 hour; per-run override via detached: { noProgressBudgetMs }). This surfaces as status: "interrupted", reason: "no-progress". A child that never reports is bounded only by the absolute detachedMaxBudgetMs ceiling — a run is never given up on merely for being slow. Set noProgressBudgetMs to 0 or Infinity to disable the resetting window for a run.

Render child timelines in React

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. Each run state carries progress and milestones, so a background-runs tray can render a live bar, phase, and milestone chips without drilling in.

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.

Drill in and gate access

Agents as 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 });
    }
}

Clear retained runs

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.

Interrupted runs and recovery

Agent-tool runs are retained in the parent. If the parent restarts (deploy or eviction) while a child run is still starting or running, it does not abandon the child. Startup recovery re-attaches to the live child and tails its stream to the child's terminal result. Because the child is a sub-agent with its own chatRecovery, it self-heals its own interrupted turn while the parent forwards its output. A completed child is finalized without re-running finished work.

The re-attach wait is progress-keyed, not a fixed wall clock. Two static options tune it:

Option Default Behavior
agentToolReattachNoProgressTimeoutMs 120000 (2 min) How long the parent waits with no forward progress before giving up. Resets on every forwarded chunk, so a streaming child is followed through to terminal.
agentToolReattachMaxWindowMs Infinity Optional hard wall-clock ceiling on a single re-attach. Uncapped by default (mirrors chat recovery's maxRecoveryWork), so a healthy, long-running child is never cut off. Set a finite value to impose a cap.

Give-up outcomes map to the AgentToolFailure fields:

  • A child that goes silent for a full no-progress window is sealed reason: "no-progress", childStillRunning: true. This seal is soft: the child is left running, so re-dispatching the same runId can re-attach and collect it if it self-heals.
  • If you set a finite agentToolReattachMaxWindowMs and it fires, the run is sealed reason: "window-exceeded", childStillRunning: false, and the child is torn down (it has had its full window and is treated as exhausted).
  • A child that cannot be tailed or inspected, or that exceeds the overall recovery deadline, is sealed with the matching reason so the parent tool call returns a structured failure instead of hanging indefinitely.

A hung child can never block recovery forever. The no-progress budget bounds a silent child. A content runaway is bounded by the child's own chatRecovery (maxRecoveryWork and shouldKeepRecovering), not by a parent-only timer.

Monitor parent reconciliation through the agentTool observability channel:

import { subscribe } from "agents/observability";

const unsubscribe = subscribe("agentTool", (event) => {
    if (event.type === "agent_tool:recovery:row") {
        console.log("Recovered agent-tool row", event.payload);
    }
});
import { subscribe } from "agents/observability";

const unsubscribe = subscribe("agentTool", (event) => {
    if (event.type === "agent_tool:recovery:row") {
        console.log("Recovered agent-tool row", event.payload);
    }
});

Raw diagnostics_channel subscribers should use the channel name agents:agent_tool.

Example

Agents as tools example

Run chat-capable sub-agents as retained tools, stream their timelines inline, and drill into child agents.

Sub-agents

Spawn child agents with isolated storage, typed RPC, and nested client routing.

Chat agents

Build AI chat interfaces with AIChatAgent and useAgentChat.

Was this helpful?

YesNo

On this page

Agents as tools·续 - 图1Docs

{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/runtime/execution/agent-tools/#page","headline":"Agents as tools","description":"Run Think and AIChatAgent sub-agents as retained, streaming tools from a parent agent.","url":"https://developers.cloudflare.com/agents/runtime/execution/agent-tools/","inLanguage":"en","image":"https://developers.cloudflare.com/agents/runtime/execution/agent-tools/og.png?v=b728c84fec3af14d","dateModified":"2026-06-26","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}


description: Run work that survives Durable Object eviction with runFiber(), startFiber(), keepAlive(), and crash recovery. title: Durable execution with fibers image: https://developers.cloudflare.com/agents/runtime/execution/durable-execution/og.png?v=a2889c46d9e8ecae


Skip to content

Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/agents/llms.txt
Use this file to discover all available pages before exploring further.