Durable recovery·续

Override repairInterruptedToolPart to customize the repaired shape. The common case is a client-resolved tool — for example an ask_user question that has no server execute and is normally answered by the user's next message. Converting it to a plain text part lets the model treat it as ordinary conversation rather than a tool error, and keeps the question verbatim through compaction:

export class MyAgent extends Think {
    repairInterruptedToolPart(part) {
        const record = part;
        if (record.type === "tool-ask_user") {
            const input = record.input;
            if (input?.prompt) {
                return { type: "text", text: input.prompt };
            }
        }
        return super.repairInterruptedToolPart(part);
    }
}
import type { UIMessage } from "ai";

export class MyAgent extends Think<Env> {
    protected override repairInterruptedToolPart(
        part: UIMessage["parts"][number],
    ): UIMessage["parts"][number] {
        const record = part as Record<string, unknown>;
        if (record.type === "tool-ask_user") {
            const input = record.input as { prompt?: string } | undefined;
            if (input?.prompt) {
                return { type: "text", text: input.prompt };
            }
        }
        return super.repairInterruptedToolPart(part);
    }
}

This runs during transcript repair — before the repaired transcript is persisted and sent to the model — so the conversion shapes the current turn, not just the next one. The input is already normalized to a valid object. A returned tool part must carry a settled result (output-available, output-error, or output-denied); returning a non-tool part such as text is also fine.

Context-window overflow recovery

Compaction is checked between turns — compactAfter() runs after each appendMessage(). But a single long, tool-heavy turn grows the prompt step by step inside one streamText loop and can exceed the model context window mid-turn, before the next pre-turn check. The provider then rejects the request ("prompt is too long", context_length_exceeded), and the turn would otherwise die terminally.

Think recovers from this with two opt-in, provider-agnostic layers, both configured through the contextOverflow property. Both are off by default, so existing behavior is unchanged. Both reuse your session's compaction function, so they require a configureSession() with onCompaction() configured. Both require classifyChatError to tell Think which errors are overflows — Think ships no provider-specific matching in core.

1. Reactive backstop — contextOverflow.reactive. When a turn fails with an error you classify as "context_overflow", Think discards the truncated partial, runs session.compact(), and re-runs the turn from the compacted history. The partial is not persisted: the turn restarts from scratch, so keeping the cut-off assistant message would orphan it beside the recovered answer. It is bounded by contextOverflow.maxRetries (default 1); if compaction cannot shorten history or the budget is spent, the overflow surfaces terminally through onChatError with classification: "context_overflow" — it never loops or ends silently.

import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";

export class MyAgent extends Think {
    contextOverflow = { reactive: true };

// The bundled classifier covers the common providers (Anthropic, OpenAI,
    // Google, Bedrock, …). Assign it directly, or write your own.
    classifyChatError = defaultContextOverflowClassifier;
}
import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";

export class MyAgent extends Think<Env> {
    override contextOverflow = { reactive: true };

// The bundled classifier covers the common providers (Anthropic, OpenAI,
    // Google, Bedrock, …). Assign it directly, or write your own.
    override classifyChatError = defaultContextOverflowClassifier;
}

2. Proactive guard — contextOverflow.proactive. Heads off the provider error before it happens. Before each step, Think reads the previous step's model-reported usage.inputTokens (provider-agnostic) and, if it crosses maxInputTokens * (headroom ?? 0.9), compacts in place and feeds the recompacted history into the upcoming step. If a provider omits inputTokens, it falls back to usage.totalTokens (a safe over-approximation — it compacts slightly early rather than missing the threshold). It compacts at most proactive.maxCompactions times per turn (default 1) — independent of the reactive maxRetries budget — so a history that cannot shorten does not compact on every step.

import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";

export class MyAgent extends Think {
    contextOverflow = {
        reactive: true,
        // Compact mid-turn once a step approaches 90% of a 200K window.
        proactive: { maxInputTokens: 200_000 },
    };

classifyChatError = defaultContextOverflowClassifier;
}
import { Think, defaultContextOverflowClassifier } from "@cloudflare/think";

export class MyAgent extends Think<Env> {
    override contextOverflow = {
        reactive: true,
        // Compact mid-turn once a step approaches 90% of a 200K window.
        proactive: { maxInputTokens: 200_000 },
    };

override classifyChatError = defaultContextOverflowClassifier;
}

Use either layer alone, or both together: the proactive guard avoids most overflows, and the reactive backstop catches any that still slip through (for example, a turn that starts already over budget, or a single tool result so large that compaction cannot help — in which case it terminalizes cleanly). Both apply to every turn entry path (WebSocket, sub-agent chat(), and programmatic saveMessages() / submitMessages()), and both emit a chat:context:compacted diagnostics channel event.

Note

A no-op compaction cannot rescue an over-budget turn, so recovery is only as effective as your compaction configuration. For tool-heavy histories, configure a tokenCounter on compactAfter() (refer to Sessions).

For a runnable demo against a real Workers AI model, refer to the context-overflow-recovery example ↗︎.

Stability detection

Think provides methods to check if the agent is in a stable state — no pending tool results, no pending approvals, no active turns.

hasPendingInteraction

Returns true if any assistant message has pending tool calls (tools without results or pending approvals).

protected hasPendingInteraction(): boolean

waitUntilStable

Returns a promise that resolves to true when the agent reaches a stable state, or false if the timeout is exceeded.

const stable = await this.waitUntilStable({ timeout: 30_000 });
if (stable) {
    await this.saveMessages([
        {
            id: crypto.randomUUID(),
            role: "user",
            parts: [{ type: "text", text: "Now that you are done, summarize." }],
        },
    ]);
}
const stable = await this.waitUntilStable({ timeout: 30_000 });
if (stable) {
    await this.saveMessages([
        {
            id: crypto.randomUUID(),
            role: "user",
            parts: [{ type: "text", text: "Now that you are done, summarize." }],
        },
    ]);
}

Was this helpful?

YesNo

On this page

Durable recovery·续 - 图1Docs

{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/agents/harnesses/think/recovery/#page","headline":"Durable recovery","description":"Bounded chat recovery, the stream-stall watchdog, repairing interrupted tool calls, and stability detection for Think agents.","url":"https://developers.cloudflare.com/agents/harnesses/think/recovery/","inLanguage":"en","image":"https://developers.cloudflare.com/agents/harnesses/think/recovery/og.png?v=3f08719de29187b8","dateModified":"2026-10-22","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: Declare recurring, timezone-aware Think turns and deterministic handlers with getScheduledTasks() and a typed scheduling DSL. title: Scheduled tasks image: https://developers.cloudflare.com/agents/harnesses/think/scheduled-tasks/og.png?v=4fbd04c27070ed65


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.