Background sub-agents get durable progress and milestones
The latest release of the Agents SDK ↗ makes it easier to run long work in the background, drive turns through one entry point, and keep chat agents working through deploys, evictions, and reconnects.
This release adds first-class detached (background) sub-agent runs with live progress and durable milestones, a single runTurn turn-admission entry point, and a large round of recovery and reliability fixes that continue converging @cloudflare/think and @cloudflare/ai-chat onto one model.
Background sub-agents with progress and milestones
runAgentTool can now dispatch a sub-agent without blocking the calling turn. A detached run returns a handle immediately and is owned by a durable, eviction-surviving backbone instead of being abandoned when the dispatching turn ends.
<span class="line"><span class="nb-shiki-1itgoe">class</span><span class="nb-shiki-1t8gfj"> OrdersAgent</span><span class="nb-shiki-1itgoe"> extends</span><span class="nb-shiki-1t8gfj"> Think</span><span class="nb-shiki-140thh"> {</span></span>
<span class="line"><span class="nb-shiki-1itgoe"> async</span><span class="nb-shiki-1t8gfj"> startImport</span><span class="nb-shiki-140thh">(</span><span class="nb-shiki-1jdh33">input</span><span class="nb-shiki-140thh">) {</span></span>
<span class="line"><span class="nb-shiki-21nrsd"> // Fire-and-forget, or wire a durable completion callback</span></span>
<span class="line"><span class="nb-shiki-21nrsd"> // (by method name, like schedule()):</span></span>
<span class="line"><span class="nb-shiki-1itgoe"> await</span><span class="nb-shiki-dzsirb"> this</span><span class="nb-shiki-140thh">.</span><span class="nb-shiki-1t8gfj">runAgentTool</span><span class="nb-shiki-140thh">(ImportAgent, {</span></span>
<span class="line"><span class="nb-shiki-140thh"> input,</span></span>
<span class="line"><span class="nb-shiki-140thh"> detached: { onFinish: </span><span class="nb-shiki-mdbnqw">"onImportDone"</span><span class="nb-shiki-140thh">, maxBudgetMs: </span><span class="nb-shiki-dzsirb">60</span><span class="nb-shiki-1itgoe"> *</span><span class="nb-shiki-dzsirb"> 60</span><span class="nb-shiki-1itgoe"> *</span><span class="nb-shiki-dzsirb"> 1000</span><span class="nb-shiki-140thh"> },</span></span>
<span class="line"><span class="nb-shiki-140thh"> });</span></span>
<span class="line"><span class="nb-shiki-140thh"> }</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-21nrsd"> // result.status: "completed" | "error" | "aborted" | "interrupted"</span></span>
<span class="line"><span class="nb-shiki-1itgoe"> async</span><span class="nb-shiki-1t8gfj"> onImportDone</span><span class="nb-shiki-140thh">(</span><span class="nb-shiki-1jdh33">run</span><span class="nb-shiki-140thh">, </span><span class="nb-shiki-1jdh33">result</span><span class="nb-shiki-140thh">) {}</span></span>
<span class="line"><span class="nb-shiki-140thh">}</span></span>
<span class="line"><span class="nb-shiki-1itgoe">class</span><span class="nb-shiki-1t8gfj"> OrdersAgent</span><span class="nb-shiki-1itgoe"> extends</span><span class="nb-shiki-1t8gfj"> Think</span><span class="nb-shiki-140thh"> {</span></span>
<span class="line"><span class="nb-shiki-1itgoe"> async</span><span class="nb-shiki-1t8gfj"> startImport</span><span class="nb-shiki-140thh">(</span><span class="nb-shiki-1jdh33">input</span><span class="nb-shiki-140thh">) {</span></span>
<span class="line"><span class="nb-shiki-21nrsd"> // Fire-and-forget, or wire a durable completion callback</span></span>
<span class="line"><span class="nb-shiki-21nrsd"> // (by method name, like schedule()):</span></span>
<span class="line"><span class="nb-shiki-1itgoe"> await</span><span class="nb-shiki-dzsirb"> this</span><span class="nb-shiki-140thh">.</span><span class="nb-shiki-1t8gfj">runAgentTool</span><span class="nb-shiki-140thh">(ImportAgent, {</span></span>
<span class="line"><span class="nb-shiki-140thh"> input,</span></span>
<span class="line"><span class="nb-shiki-140thh"> detached: { onFinish: </span><span class="nb-shiki-mdbnqw">"onImportDone"</span><span class="nb-shiki-140thh">, maxBudgetMs: </span><span class="nb-shiki-dzsirb">60</span><span class="nb-shiki-1itgoe"> *</span><span class="nb-shiki-dzsirb"> 60</span><span class="nb-shiki-1itgoe"> *</span><span class="nb-shiki-dzsirb"> 1000</span><span class="nb-shiki-140thh"> },</span></span>
<span class="line"><span class="nb-shiki-140thh"> });</span></span>
<span class="line"><span class="nb-shiki-140thh"> }</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-21nrsd"> // result.status: "completed" | "error" | "aborted" | "interrupted"</span></span>
<span class="line"><span class="nb-shiki-1itgoe"> async</span><span class="nb-shiki-1t8gfj"> onImportDone</span><span class="nb-shiki-140thh">(</span><span class="nb-shiki-1jdh33">run</span><span class="nb-shiki-140thh">, </span><span class="nb-shiki-1jdh33">result</span><span class="nb-shiki-140thh">) {}</span></span>
<span class="line"><span class="nb-shiki-140thh">}</span></span>
Highlights:
- Durable, exactly-once-on-the-happy-path completion via a warm fast path plus a self-scheduling reconcile backbone that survives eviction and deploys.
- Bounded. An absolute
maxBudgetMsceiling (default 24h) andcancelAgentTool(runId)keep abandoned runs from holding a concurrency slot forever. detached: { notify: true }lets a finished background run inject a message back into the chat so the model reacts to the result — no hand-wiredonFinishneeded.
Sub-agents can also report mid-run progress that rides their own turn stream back to the parent's connected clients:
<span class="line"><span class="nb-shiki-21nrsd">// Inside the child sub-agent:</span></span>
<span class="line"><span class="nb-shiki-1itgoe">await</span><span class="nb-shiki-dzsirb"> this</span><span class="nb-shiki-140thh">.</span><span class="nb-shiki-1t8gfj">reportProgress</span><span class="nb-shiki-140thh">({</span></span>
<span class="line"><span class="nb-shiki-140thh"> fraction: </span><span class="nb-shiki-dzsirb">0.6</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh"> phase: </span><span class="nb-shiki-mdbnqw">"deploying"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh"> message: </span><span class="nb-shiki-mdbnqw">"Generating menu page…"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh">});</span></span>
<span class="line"><span class="nb-shiki-21nrsd">// Inside the child sub-agent:</span></span>
<span class="line"><span class="nb-shiki-1itgoe">await</span><span class="nb-shiki-dzsirb"> this</span><span class="nb-shiki-140thh">.</span><span class="nb-shiki-1t8gfj">reportProgress</span><span class="nb-shiki-140thh">({</span></span>
<span class="line"><span class="nb-shiki-140thh"> fraction: </span><span class="nb-shiki-dzsirb">0.6</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh"> phase: </span><span class="nb-shiki-mdbnqw">"deploying"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh"> message: </span><span class="nb-shiki-mdbnqw">"Generating menu page…"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh">});</span></span>
Progress surfaces on AgentToolRunState.progress via useAgentToolEvents, so a background-runs tray can render a live bar without drilling in, and the latest snapshot is persisted for inspection after eviction. Naming a milestone promotes a signal to a durable, replayable row, and detached: { onMilestones } can surface a milestone as a synthetic chat message ("narrate" for a cheap status line, or "react" to drive a model turn).
One entry point for turns: runTurn
@cloudflare/think adds a public runTurn(options) facade that unifies turn admission behind a single mode:
<span class="line"><span class="nb-shiki-1itgoe">await</span><span class="nb-shiki-dzsirb"> this</span><span class="nb-shiki-140thh">.</span><span class="nb-shiki-1t8gfj">runTurn</span><span class="nb-shiki-140thh">({ mode: </span><span class="nb-shiki-mdbnqw">"wait"</span><span class="nb-shiki-140thh">, messages }); </span><span class="nb-shiki-21nrsd">// saveMessages / continueLastTurn</span></span>
<span class="line"><span class="nb-shiki-1itgoe">await</span><span class="nb-shiki-dzsirb"> this</span><span class="nb-shiki-140thh">.</span><span class="nb-shiki-1t8gfj">runTurn</span><span class="nb-shiki-140thh">({ mode: </span><span class="nb-shiki-mdbnqw">"submit"</span><span class="nb-shiki-140thh">, messages }); </span><span class="nb-shiki-21nrsd">// durable submitMessages</span></span>
<span class="line"><span class="nb-shiki-1itgoe">await</span><span class="nb-shiki-dzsirb"> this</span><span class="nb-shiki-140thh">.</span><span class="nb-shiki-1t8gfj">runTurn</span><span class="nb-shiki-140thh">({ mode: </span><span class="nb-shiki-mdbnqw">"stream"</span><span class="nb-shiki-140thh">, messages }); </span><span class="nb-shiki-21nrsd">// chat()</span></span>
<span class="line"><span class="nb-shiki-1itgoe">await</span><span class="nb-shiki-dzsirb"> this</span><span class="nb-shiki-140thh">.</span><span class="nb-shiki-1t8gfj">runTurn</span><span class="nb-shiki-140thh">({ mode: </span><span class="nb-shiki-mdbnqw">"wait"</span><span class="nb-shiki-140thh">, messages }); </span><span class="nb-shiki-21nrsd">// saveMessages / continueLastTurn</span></span>
<span class="line"><span class="nb-shiki-1itgoe">await</span><span class="nb-shiki-dzsirb"> this</span><span class="nb-shiki-140thh">.</span><span class="nb-shiki-1t8gfj">runTurn</span><span class="nb-shiki-140thh">({ mode: </span><span class="nb-shiki-mdbnqw">"submit"</span><span class="nb-shiki-140thh">, messages }); </span><span class="nb-shiki-21nrsd">// durable submitMessages</span></span>
<span class="line"><span class="nb-shiki-1itgoe">await</span><span class="nb-shiki-dzsirb"> this</span><span class="nb-shiki-140thh">.</span><span class="nb-shiki-1t8gfj">runTurn</span><span class="nb-shiki-140thh">({ mode: </span><span class="nb-shiki-mdbnqw">"stream"</span><span class="nb-shiki-140thh">, messages }); </span><span class="nb-shiki-21nrsd">// chat()</span></span>
stream mode accepts array and function inputs to match wait mode, and all entry points now route through a shared internal admission path that throws a clear error on nested blocking admissions that previously could deadlock.
Recovery and reliability
A large part of this release continues hardening recovery and converging @cloudflare/think and @cloudflare/ai-chat onto one model:
- Stream stall watchdog.
AIChatAgentcan detect and recover from a hung model/transport stream via the opt-inchatStreamStallTimeoutMswatchdog. WithchatRecoveryenabled the stall routes into the same bounded-recovery machinery a deploy or eviction uses; otherwise it surfaces as a terminal stream error so the spinner clears. - Interrupted tool-call repair.
AIChatAgentnow repairs a transcript with a dead server-tool call before re-entering inference (parity with@cloudflare/think), so a recovered turn no longer fails withAI_MissingToolResultsError. An overridablerepairInterruptedToolPart(part)hook lets apps customize the repaired shape. - Stuck status after reconnect. Fixed AI SDK
statusgetting stuck when a reconnect races a turn that has been accepted but has not started streaming yet, so the UI now renders the in-flight turn instead of settling onready. - Live "recovering…" on connect.
AIChatAgentnow replays the recovering status to a client that connects mid-recovery, souseAgentChat'sisRecoveringreflects in-progress recovery immediately instead of appearing frozen. - Terminal connection failures. The client stops reconnecting on terminal WebSocket close events and exposes them via
connectionError/onConnectionErroronAgentClient,useAgent, anduseAgentChat. - Agent-tool child recovery. A healthy long-running sub-agent run is no longer abandoned as
interruptedafter a deploy (both@cloudflare/thinkandAIChatAgent). - Workflows from sub-agent facets. Agent Workflows can now start from sub-agent facets, with callbacks and Workflow RPC routed back to the originating facet.
- Plus forward-progress crediting convergence, broadcast-first give-up ordering, an event-driven auto-continuation barrier, and structured row-size compaction in
AIChatAgent.
Other improvements
- Shared chat React core. A new
agents/chat/reactentry exposesuseAgentChat, transport helpers, and shared wire types, withsyncMessagesToServerfor server-authoritative transcript storage.@cloudflare/think/reactand@cloudflare/ai-chat/reactare now thin wrappers over it. - Optional
aipeer. The rootagentsand@cloudflare/codemoderuntimes no longer reference AI SDK types, so they bundle withoutai/zodinstalled; AI-specific entry points still require the peer when imported.just-bashlikewise moves to an optional peer used only by the skills bash runner. - Code Mode. The default
DynamicWorkerExecutortimeout increases from 30s to 60s, executions now dispose the dynamically-loaded Worker and its RPC stub after each run (fixing a flaky isolate-shutdown assertion), connector imports are cleaned up, and the outer MCP tool-call context is passed toopenApiMcpServerrequest callbacks. - Voice. Voice turns now support AI SDK
fullStreamresponses (and warn whentextStreamis used). - MCP.
McpAgentserver-to-client requests can now be sent from callbacks that do not inherit the agent's async context, including callbacks reached through Worker Loader RPC. - Experimental: server actions and channels. This release lays groundwork for guarded server actions (
action()/getActions()with a durable replay ledger and approvals) and a unified channels surface (configureChannels(),deliverNotice()). Both are experimental and their APIs may change, so we don't recommend depending on them yet.
Upgrade
To update to the latest version:
if (!customElements.get("nb-pm-restore")) { customElements.define( "nb-pm-restore", class extends HTMLElement { connectedCallback() { const card = this.closest("[data-nb-pm]"); if (!card) return; let saved; try { saved = sessionStorage.getItem("ui-pm-tab"); } catch { return; } if (!saved) return; const tabs = card.querySelectorAll("[data-nb-pm-tab]"); let idx = -1; tabs.forEach(function (t, i) { if (t.textContent.trim() === saved) idx = i; }); if (idx < 1) return; tabs.forEach(function (t, i) { t.setAttribute("aria-selected", String(i === idx)); }); card.querySelectorAll("[data-nb-pm-panel]").forEach(function (p, i) { p.hidden = i !== idx; }); } }, ); }
npmyarnpnpmbun
<span class="text-success">npm</span><span class="text-warning"> i agents@latest @cloudflare/think@latest @cloudflare/ai-chat@latest @cloudflare/codemode@latest @cloudflare/voice@latest</span>
<span class="text-success">yarn</span><span class="text-warning"> add agents@latest @cloudflare/think@latest @cloudflare/ai-chat@latest @cloudflare/codemode@latest @cloudflare/voice@latest</span>
<span class="text-success">pnpm</span><span class="text-warning"> add agents@latest @cloudflare/think@latest @cloudflare/ai-chat@latest @cloudflare/codemode@latest @cloudflare/voice@latest</span>
<span class="text-success">bun</span><span class="text-warning"> add agents@latest @cloudflare/think@latest @cloudflare/ai-chat@latest @cloudflare/codemode@latest @cloudflare/voice@latest</span>
Refer to the Think documentation, Code Mode documentation, and Agents documentation for more information.
Fetched July 22, 2026



