agents
0.29.0 · 20 Oct 2026MIT1.5M / weekRepositorynpmAtom feed
205 stable releases, newest first, by release line. Notes are quoted as the maintainers published them.
- 0.29.x · latest line
0.29.0
20 Oct 2026- deprecated
Chat recovery now seals a turn after
chatRecovery.maxInterruptedAttemptsinterrupted attempts (default 5), so a turn that exceeds the Durable Object memory limit on every run no longer retries forever;maxOomRetriesis deprecated. See Stream Recovery. Agent Skills read and write a
@cloudflare/computerWorkspace, for both projection andskills.runner(). See Upgrading to a Computer workspace.Browsercreates and closes its Browser Run sessions with the binding's typedacquire()andcloseSession(), falling back to the HTTP API in fully local dev. See Browser.
Minor Changes
Agent Skills read and write a
@cloudflare/computerWorkspace, for both projection andskills.runner(). See Upgrading to a Computer workspace.
Patch Changes
Browsercreates and closes its Browser Run sessions with the binding's typedacquire()andcloseSession(), falling back to the HTTP API in fully local dev. See Browser.- deprecated
Chat recovery now seals a turn after
chatRecovery.maxInterruptedAttemptsinterrupted attempts (default 5), so a turn that exceeds the Durable Object memory limit on every run no longer retries forever;maxOomRetriesis deprecated. See Stream Recovery. createAItakes{ accountId, apiToken }in place ofbinding, to use a gateway in another account or to run outside a Worker. See the models docs.createAItranscribes with@cf/deepgram/nova-3. See the models docs.createAIno longer uses AI Gateway's deprecated Universal Endpoint. Model ids, now including catalog ids likeanthropic/claude-opus-4.8, run onenv.AI.run; model objects go to the provider-native path. AddsbyokAlias, fixes pi-ai catalog models and dynamic routes, and letsagents/models/opencodetake catalog ids. See the models docs.createAIresumes a catalog or dynamic route stream that drops mid-answer, with nothing lost or repeated. See the models docs.agents/models/pi-airesolves Claude ids from pi-ai 1.2's gateway catalog under AI Gateway's spelling,anthropic/claude-opus-4.8. See Models for pi-ai.Support Pi 1.3: the Pi web search, web fetch, browser, and skills tools return
output, and the Pi peer range is now~1.3.0. See Pi harness.PiHarnesssubmit()andsteer()now accept pi-durable'sentryoption, which labels an input with a transcript kind of your own (for exampleapp.approval) instead ofpi.user. Before, the harness had no way to pass it. See Label input the user did not type.openPiSessionStorenow uses pi-durable's own Durable Object SQLite adapter, so several Pi storages in one object share its operation queue. See Pi harness.
- 0.28.x
0.28.1
13 Oct 2026createAI(agents/models/ai-sdk,agents/models/pi-ai) adds thezdrgateway option, andvendorKey: "request"for AI SDK models routed with your own vendor key. See docs/agents/models.md.createAI(agents/models/ai-sdk,agents/models/pi-ai) takes AI Gateway dynamic routes asai("dynamic/<route>"). See docs/agents/models.md.Update
@modelcontextprotocol/clientand@modelcontextprotocol/serverto 2.2.0 and@modelcontextprotocol/sdkto 1.31.0. See Model Context Protocol.
Patch Changes
createAI(agents/models/ai-sdk,agents/models/pi-ai) adds thezdrgateway option, andvendorKey: "request"for AI SDK models routed with your own vendor key. See docs/agents/models.md.createAI(agents/models/ai-sdk,agents/models/pi-ai) takes AI Gateway dynamic routes asai("dynamic/<route>"). See docs/agents/models.md.Update
@modelcontextprotocol/clientand@modelcontextprotocol/serverto 2.2.0 and@modelcontextprotocol/sdkto 1.31.0. See Model Context Protocol.Update
@modelcontextprotocol/clientand@modelcontextprotocol/serverto 2.3.1 and@modelcontextprotocol/sdkto 1.32.1. The MCP client transports now follow redirects only within the same origin. See Model Context Protocol.McpAgentnow answers requests for a session ended withDELETEwith 404 straight away, instead of serving it until deferred teardown runs, andcreateMcpHandlerrejects a Legacy request whose factory returns an instance still serving another request without closing that instance. See Transport.OpenCodeHarness:session.events()now returns a stream with a snapshot of the session and then its events, translated into the same kindsPiHarnessreports:run_start/run_end,operation_start/operation_end(with the resultwait()returns),message_start/message_end,text_delta,reasoning_delta,tool_start/tool_end, permissions and compaction. It replaces the async iterable of OpenCode's raw events; those are still available, durable and resumable, fromsession.log(). The OpenCode example now gives the model Workspace tools from@cloudflare/computerin place of OpenCode's local file and shell tools.
0.28.0
9 Oct 2026Add
browserTool()toagents/browser/pi. See Persistent browser.Make
agents/lifecyclestable along with theScheduler(agents/schedules),State(agents/state),WebSockets(agents/websockets) andMCPClientManager(agents/mcp/client) capabilities. See Lifecycle.ThinkHarnessnow keeps its sessions and operations in the sharedagents/harness/store. Migrations are performed on first run. See ThinkHarness: Recovery.
This release adds two new tools,
browserviaagents/browser/{pi,ai-sdk,tanstack-ai}allowing an agent to create and manipulate a persistent browser session andweb_fetchviaagents/webfetch/{pi,ai-sdk,tanstack-ai}that can retrieve most document formats converted into markdown.Minor Changes
Add
browserTool()toagents/browser/pi. See Persistent browser.Make
agents/lifecyclestable along with theScheduler(agents/schedules),State(agents/state),WebSockets(agents/websockets) andMCPClientManager(agents/mcp/client) capabilities. See Lifecycle.
Patch Changes
ThinkHarnessnow keeps its sessions and operations in the sharedagents/harness/store. Migrations are performed on first run. See ThinkHarness: Recovery.Add
agents/webfetch, aweb_fetchtool that reads a URL as Markdown, JSON, or text (HTML, PDF, and Office documents via Worker AI). See Fetch the Web.
- 0.27.x
0.27.0
7 Oct 2026- breaking
remove the first pass of
agents/channelsand move Channels toagents/experimental/channels.The
agents/channelsentry points are gone, along withChannelHost, thefallbackandfanoutcomposites, and the AI SDK, TanStack AI and Voice helpers from the first pass. Slack, Telegram and Email move toagents/experimental/channels/slack,agents/experimental/channels/telegramandagents/experimental/channels/email, and keep verified ingress and normalization. Channels is being rebuilt around conversations and turns; the new API is experimental and may change between releases. New experimental harnesses:
AiSdkHarness,ThinkHarness,ContainerHarness(which runs Claude Code or Codex in a Container) andOpenCodeHarness, all with the same shape asPiHarness.New
agents/websearch: aweb_searchtool over Cloudflare's Web Search API for pi, the AI SDK and TanStack AI.
This release adds four new experimental harnesses and a
web_searchtool, and reorganizes the experimental Channels API.New experimental harnesses:
AiSdkHarness,ThinkHarness,ContainerHarness(which runs Claude Code or Codex in a Container) andOpenCodeHarness, all with the same shape asPiHarness.New
agents/websearch: aweb_searchtool over Cloudflare's Web Search API for pi, the AI SDK and TanStack AI.Channels moved to
agents/experimental/channels: Channels is being rebuilt around conversations and turns, and the oldagents/channelsentry point has been removed. The new API includesChannelGateway, the Web Channel and its client, an AI SDK chat transport, and a newnpx agents tuiterminal client.Fixes: Streams, Tasks,
useAgentChatreconnects,WebChannelClient, MCP credential cleanup onremoveServer(), and x402 with MCP SDK v2.
Minor Changes
Add
AiSdkHarnessfrom the newagents/harness/ai-sdkentry point, which runs each message withstreamTextand keeps sessions and transcripts in the Durable Object, soChannels.forHarnesscan serve an AI SDK model. The same entry point exports the AI SDK message conversions it is built on andcreateSendMessageTool, an AI SDK tool that sends a message to a surface through the gateway.Add
Channelstoagents/experimental/channels: a Lifecycle capability that serves an agent harness's sessions as conversations to every surface that joins them, with durable, resumable responses onStreams. Connect a harness withChannels.forHarness(harness, { channels }). The entry point also exports the turn protocol types and a draft harness interface (AgentHarness), which may change.- breaking
remove the first pass of
agents/channelsand move Channels toagents/experimental/channels.The
agents/channelsentry points are gone, along withChannelHost, thefallbackandfanoutcomposites, and the AI SDK, TanStack AI and Voice helpers from the first pass. Slack, Telegram and Email move toagents/experimental/channels/slack,agents/experimental/channels/telegramandagents/experimental/channels/email, and keep verified ingress and normalization. Channels is being rebuilt around conversations and turns; the new API is experimental and may change between releases. Add
ChannelGatewaytoagents/experimental/channels: the Worker entry point that verifies channel webhooks, takes Web Channel upgrades throughweb()fromagents/experimental/channels/web, routes each to the agent object that holds its conversation, and delivers outbound messages. Every Channel that takes ingress needs aparticipantcallback, where the application says who the sender is; Channels never picks an identity. The agent object is the authorization boundary: by default each participant gets one of their own, and aroutecallback can share one between participants or refuse them.Add
WebChannelChatTransport(agents/experimental/channels/web/ai-sdk), an AI SDKChatTransportover the Web Channel, so AI SDK UIs can join a Channels conversation.Add the Web Channel (
agents/experimental/channels/web) and its client (agents/experimental/channels/web/client), which connect browsers and terminals to a conversation over the agent's WebSockets: live transcript and turns, approvals, client tool results, and creating, forking, resetting, listing and following conversations.WebSocketsgainsusefor protocol handlers that run before the configured ones.Add
agents/websearch, aweb_searchtool over Cloudflare's Web Search API for the pi harness, the AI SDK, and TanStack AI. See Search the Web.
Patch Changes
AiSdkHarness:session.wait(operationId, signal)now rejects straight away whensignalis already aborted, instead of staying pending until the operation settles.AiSdkHarnessno longer fails to start when it has stored sessions.onStartlisted sessions while listing each one's pending work, and Durable Object storage allows one openkv.list()at a time, so a restarted agent stopped acknowledging messages.AiSdkHarnesspasses its tools toconvertToModelMessages, so a tool'stoModelOutputalso shapes its results on later turns instead of the model getting the raw output as JSON.browserToolnow has its own tool description instead of codemode's generic one. It covers thecdpAPI, common CDP mistakes, and the run time limit, so the model no longer runscodemode.searchfirst. It also explains how to take a smaller screenshot when one is over the 1 MB result limit. See Persistent browser.Add
npx agents tui <url>, a terminal client for a conversation's Web Channel.It streams the transcript with highlighted Markdown, collapsible reasoning and tool cards, and shows messages and turns from every surface live. It answers approvals inline, lets a person type a client tool's result, and cancels the running turn with Esc.
--headeradds headers to the WebSocket upgrade, andCF_ACCESS_CLIENT_ID/CF_ACCESS_CLIENT_SECRETare sent as an Access service token. Its dependencies are bundled intodist/cli.js, so installingagentsadds none.Add experimental
agents/harness/container,agents/harness/container/daemon,agents/harness/container/runtimeandagents/harness/store. Everything in them may change before it stabilizes.ContainerHarnessruns an agent CLI such as Claude Code or Codex in a Cloudflare Container (ctx.container) and drives it from a Durable Object, with the same interface asPiHarness(prompt,submit,wait,abort,messages,pending,session(id),sessions). Typed presets (claudeCode(),codex(),containerAgent()) say what runs: the harness builds the container fromcloudflare/debian-trixieat runtime and snapshots it, so there is no image to build, andContainerEgressadds credentials (apiKey,baseUrl,headers) outside the container, which only ever sees a placeholder key. The container stops after an idle timeout (five minutes by default) and the next prompt starts a new one in which the CLI resumes its own session; a container lost mid-run settles that run ascontainer_lost(or reruns it withonContainerLost: "retry"), and an object evicted mid-run reattaches and replays what it missed.agents/harness/container/runtimeis the daemon's Node runtime withcliAdapter, for images that bring their own CLI;agents/harness/container/daemonis its runtime-agnostic core; andagents/harness/storeis a session store on the object's SQLite that any harness can use.MCPClientManager.removeServer()(andAgent.removeMcpServer()) now clears the server's saved OAuth state from Durable Object storage: tokens and client information (including those under earlier client IDs), verifiers, pending states and discovery state. This also works when the server's connection was already closed. Other servers' credentials are untouched, and tokens are not revoked at the OAuth provider. Custom auth providers getinvalidateCredentials("all").Add experimental
agents/harness/opencodeandagents/models/opencode.OpenCodeHarnesshosts OpenCode v2 (@opencode/sdk/workerd) in a Durable Object behind the same interface asPiHarness: OpenCode's tables are kept under anopencode_prefix, a Lifecycle wake job per session brings runs back after eviction, and the harness closes OpenCode when it is idle so the object can hibernate.createAI({ binding })fromagents/models/opencodereturns an OpenCode plugin that serves Workers AI models through theAIbinding. Both APIs are experimental and will change.Streams: a live
read/readBatchesno longer misses chunks appended while the consumer is still handling the previous batch. It re-polls before waiting for the next append, so a reader no longer stalls until a later append or the stream's end.Streamsrechecks that the v1 chunk table still exists before using it. Two Streams instances on one object, such as a host's own and the oneThinkHarnesskeeps, each remembered the table separately, so after one folded the last v1 rows and dropped the table the other failed its next append withno such table.A routed Task wake whose owner facet is no longer in the root's registry now ends quietly instead of throwing
Invalid job outcomeon the alarm.Add
ThinkHarnessfrom the new experimentalagents/harness/thinkentry point. It runs Think's agent loop as a Lifecycle capability with the same shape asPiHarness(prompt,submit,wait,abort,sessions,session(id)), and implements the shared harness interface Channels serves.The harness owns its storage: transcripts in the Sessions tables, with
branches(),search()andcompact()on each session, and in-flight model output in the Streams tables, so a host installs only the harness. Each session has one Lifecycle wake job, so a turn interrupted by an eviction picks up from its last durable write: a cut-short model call is rebuilt from its stream and continued in the same message, and a cut-short tool call is reported to the model, or rerun when the tool carriesrecovery: "rerun".ThinkChatserves a session over Think'suseAgentChatWebSocket protocol. See Think harness.agents tuinow handles Cloudflare Access: for a URL behind Access, it gets a token fromcloudflared(logging in through the browser the first time) and sends it on the WebSocket upgrade.CF_ACCESS_CLIENT_ID/CF_ACCESS_CLIENT_SECRETand explicit--headercredentials still take precedence.agents tuidrops a#fragmentfrom the URL it is given, since a WebSocket URL cannot carry one. Previously such a URL made the client exit before connecting.useAgentChatreplaces a cut-off assistant reply with the server's copy after the WebSocket drops mid-stream. Think sends that copy on reconnect; AIChatAgent doesn't yet.WebChannelClient:send()andlistConversations()now reject once the client is closed instead of waiting forever.follow()andclose()also cancel a reconnect still waiting after a dropped connection, so it no longer opens a second socket that replaces the followed one or reconnects a closed client.withX402(...).paidToolnow reads the payment from MCP SDK v2 servers, and matches thePAYMENT-SIGNATURE/X-PAYMENTheaders case-insensitively.
- 0.26.x
0.26.0
2 Oct 2026Update pi-durable and pi-ai to
^1.0.0. See Pi harness docs for details.
- 0.25.x
0.25.0
2 Oct 2026- deprecated
Keep assistant messages and tool outputs attached to the correct turn when a provider reuses a
toolCallId;resolveToolMergeIdis deprecated in favour ofreconcileMessages. See Chat agents. Native RPC calls to async Agent and Think methods now start lifecycle initialization first; address Agents by name because raw IDs from
newUniqueId()andidFromString()now fail their first async RPC. See Lifecycle.Report a failed agent-tool child as failed even when it was evicted before recording the failure. See Agent tools.
Minor Changes
Native RPC calls to async Agent and Think methods now start lifecycle initialization first; address Agents by name because raw IDs from
newUniqueId()andidFromString()now fail their first async RPC. See Lifecycle.
Patch Changes
Report a failed agent-tool child as failed even when it was evicted before recording the failure. See Agent tools.
Fix agent-tool chunks being duplicated or dropped on reconnect, child re-attach, and fiber recovery. See Agent tools.
Add
eventDelivery: "terminal"torunAgentToolto forward only lifecycle, progress, and milestone events for a run. See Agent tools.Add experimental
agents/models/ai-sdk, a provider for Workers AI and AI Gateway over theAIbinding. See Models.Add experimental
agents/models/pi-ai, a pi-ai provider for Workers AI and AI Gateway over theAIbinding. See Models for pi-ai.Add experimental
agents/harnesses/pito host pi-durable sessions in a Durable Object. See Pi harness and the runnable example.Keep a tool call's input when its approval request arrives before the input finishes streaming (#1872). See Human in the loop.
loadCdpSpecaccepts an optionalsessionIdto read the protocol from an existing Browser Run session. See Browse the web.connectBrowserSessionthrows aBrowserRenderingErrorwith the HTTP status when Browser Run returns no WebSocket. See Browse the web.Add Browser Run hostname guardrails, close and activity callbacks, and the
CdpConnectionname while retainingCdpSessionas a deprecated alias. See Browse the web.Add persistent browser tools for AI SDK and TanStack AI, backed by experimental Browser sessions. See Browse the web.
CdpConnection.send()rejects immediately when its socket is closed instead of waiting for the timeout. See Browse the web.cdp.spec()andloadCdpSpec()now include command parameters, return types, and type details, so a model can see how to call a CDP method. See Browse the web.useAgentChatloads the new agent's history when the agent address passed touseAgentchanges (#1864, #1874). See Client SDK.useAgentChatgainsonTurnEnd, and terminal chat frames carry outcomes and user message IDs so clients can settle sends correctly across recovery and reconnects. Replay, tool callbacks, and turn state now remain consistent through terminal delivery; see Chat agents.A WebSocket close before the final
doneframe now putsuseAgentChatin theerrorstate instead of showing a truncated answer as complete (#2013). See Chat agents.Stop storing compaction summaries that clients echo back, and hide duplicates already stored (#1984). See Sessions.
Context blocks can set
whenChanged: "remind"to send changes as a reminder after the cached prompt instead of rewriting it. See Context.Sub-agent broadcasts make at most one root call, and none when no client would receive them. See Sub-agents.
Sub-agents no longer sync root-owned host jobs, which could fail a facet's first call after restart. See Sub-agents.
Fix WebSocket connections to sub-agents nested more than one level deep. See Sub-agents.
useAgentChatlets the server snapshot repair a scrambled cross-tab or resumed assistant message instead of keeping it until reload. See Chat agents.useAgentChatcallsonToolCallonly after the stream ends, and only for tool calls still waiting on the client (#2195). See Client tools.Interfaces can now be used as
PropsonAgent,AIChatAgent,Think, and routing helpers. See Routing.- deprecated
Keep assistant messages and tool outputs attached to the correct turn when a provider reuses a
toolCallId;resolveToolMergeIdis deprecated in favour ofreconcileMessages. See Chat agents. useAgentandAgentClientresolvereadyonly after the agent's state arrives, sostateis no longer brieflyundefined(#2268). See Client SDK.useAgentChatkeeps messages sent while disconnected when reconnecting, including Think transcripts, instead of erasing them (#1983). See Chat agents.Correct the documented default and unit of
ChatRecoveryConfig.maxRecoveryWork. See Chat agents: recovery work units.Clients stop reconnecting after a sub-agent rejects a WebSocket, connection errors report every close that ends reconnection, and terminal rejections use close codes
4000 + status(#2118). See Sub-agents.Fix text appearing twice when reconnecting during a tool continuation. See Resumable streaming.
onChatResponsestill fires, withrecovered: true, for a turn persisted just before a Durable Object reset (#2266, #1842). See Think lifecycle hooks.runFiber()no longer callsonFiberRecovered()for work that already finished (#2305). See Durable execution.Add
session.mirror()to keep an in-memory transcript in sync with a session's change feed. See Sessions.Keep attachment references when a message that already contains attachment pointers is written back, so its media isn't garbage-collected. See Sessions.
updateMessage()detects unchanged messages from a stored content hash, cutting reads on large messages. See Sessions.Settle approved tool calls that never ran once the conversation moves past them, so later turns don't send unresolved tool calls (#2382). See Human in the loop.
Messenger replies hold the thread lock and retain queued follow-ups during slow delivery; typing updates are serialized and settle before reply text arrives. See Think messengers.
A regeneration interrupted by a restart now recovers on its own branch. See Multi-chat.
Fix
useAgentChatthrowing "Maximum update depth exceeded" during long streamed answers (#2217). See Chat agents.With
storeTools: true, oversized tool payloads are recorded with base64 data summarized instead of being dropped. See Observability.Harden transient chat recovery across Think and AI Chat, including retry budgeting, cancellation, terminal error delivery, and recovery after restarts. See the
agents/chatrecovery helpers.deleteSubAgentnow closes the sub-agent's direct client connections, so late messages can't recreate it (#2003). See Sub-agents.Truncate older tool results after model conversion so
toModelOutputand provider-executed tools keep valid outputs (#2014). See Sessions.withX402ClientenforcesmaxPaymentValueagainst the payment requirement selected for signing, before signing or retrying the tool call. See Pay from Agents SDK.
- 0.24.x
0.24.0
18 Sep 2026WebSocketsspeaks the Agent protocol for plain hosts, and gains a Cap'n Web transport.Add the
QueueLifecycle capability (agents/queue) for durable background work. Each pushed item is a job in the Lifecycle job queue, due immediately, run from the alarm loop one at a time in push order with Lifecycle's retry, deadman, and memory-limit policy. Callbacks are registered in the constructor and typed at declaration and push;push()accepts a stableid(upsert) and per-itemretry.Agent.queue()and friends now delegate to the capability. Queued callbacks run from the alarm loop in a fresh invocation, so they no longer see the enqueuing request'sconnectionorrequestthroughgetCurrentAgent()(the agent itself is still available). Thecf_agents_queuestable and the in-isolate drain are gone; legacy rows migrate into the job queue on the next start.queue()acceptsoptions.id;dequeue,dequeueAll,dequeueAllByCallback,getQueue, andgetQueuesare now asynchronous; andQueueItem.created_atis renamedcreatedAt.LifecycleServices.starting()is replaced bystatus(), which returns"zero" | "starting" | "started".Think's workflow-notification outbox and submission drain now run as queue items; the
cf_think_workflow_notificationstable migrates and is dropped on start.Both one-shot migrations (
cf_agents_queuesin Queue,cf_think_workflow_notificationsin Think) are temporary upgrade paths and will be removed in the next minor release, by which point every started object has migrated. Deployments skipping this release should upgrade through it. Workflow-notification delivery retries with backoff capped at ten minutes (previously five) and gives up after twelve hours of continuous failure (previously never), reporting the failure throughonError.Move agent state into the opt-in
StateLifecycle capability.State was one method doing four jobs inside
Agent— validate,
persist, broadcast, notify. It moves wholesale into aState
capability that owns storage and change ordering, so any Lifecycle
host gets durable, validated state without inheritingAgent:new State({ initialState: { count: 0 }, validateStateChange: (next, source) => validate(next, source), onChanged: (state, source) => notify(state, source), });The capability owns the
cf_agents_statestate row, lazy load with an
in-memory cache, initial-state seeding, and validated persistence. It
runs only theonStarthook (versioned schema init under its owncf_agents:state_schema_versionkey) and reaches Lifecycle only forstorage— no alarm, no request path. It never touches connections.Host-owned behavior is injected, not moved:
validateStateChange
stays an overridableAgentmethod and the post-changeonChanged
hook is passed intoStateas a plain option. Synchronous and
asynchronous notification hooks are both supported.Agentkeeps itsinitialStatefield and seeds it from thestategetter (standalone
hosts passinitialStatetoStatedirectly). Broadcast and the
notification hook stay onAgent, and theonMessagestate branch
stays too; only its inner write delegates to the capability.Stateis the only owner ofcf_agents_state: it creates the table,
holds the state row, and runs the legacycf_state_was_changed
cleanup in its own versioned migration.Agentused to keep its
global schema version as a row in that table; it now lives under thecf_agents:schema_versionKV key like every other capability's
version. A DO created under the old layout has the row read once,
moved to the key, and deleted on its next construction.Agent's public API and wire protocol are unchanged:state,setState(),onStateChanged, and theCF_AGENT_STATEframes behave
identically.
Minor Changes
WebSocketsspeaks the Agent protocol for plain hosts, and gains a Cap'n Web transport.A plain Durable Object composed with
WebSocketsnow works withuseAgentandAgentClient: the capability sends the identity frame on connect (protocol: falseleaves the connect sequence to the host) and servescallables— anRpcTargetwhose prototype methods are the host's remote interface.useAgent({ transport })andAgentClient({ transport })pick the wire."cf-websocket"(default) is the hibernating socket;call()/stubsend JSONrpcframes."capnweb"runs one Cap'n Web session that carries protocol frames and servescallablesnatively:call()/stubinvoke them directly, anRpcTargetresult is a live stub, aReadableStreamstreams, calls pipeline. The Durable Object stays in memory while a capnweb connection is open. PartySocket still owns reconnection on both.@callable()decorators remain the JSON-wire interface ofAgent; an Agent wanting native calls passescallables.The experimental
?__agents_rpc=capnwebendpoint from 0.23.0 is removed.LifecycleServicesexposesnameandclassName.
Add the
QueueLifecycle capability (agents/queue) for durable background work. Each pushed item is a job in the Lifecycle job queue, due immediately, run from the alarm loop one at a time in push order with Lifecycle's retry, deadman, and memory-limit policy. Callbacks are registered in the constructor and typed at declaration and push;push()accepts a stableid(upsert) and per-itemretry.Agent.queue()and friends now delegate to the capability. Queued callbacks run from the alarm loop in a fresh invocation, so they no longer see the enqueuing request'sconnectionorrequestthroughgetCurrentAgent()(the agent itself is still available). Thecf_agents_queuestable and the in-isolate drain are gone; legacy rows migrate into the job queue on the next start.queue()acceptsoptions.id;dequeue,dequeueAll,dequeueAllByCallback,getQueue, andgetQueuesare now asynchronous; andQueueItem.created_atis renamedcreatedAt.LifecycleServices.starting()is replaced bystatus(), which returns"zero" | "starting" | "started".Think's workflow-notification outbox and submission drain now run as queue items; the
cf_think_workflow_notificationstable migrates and is dropped on start.Both one-shot migrations (
cf_agents_queuesin Queue,cf_think_workflow_notificationsin Think) are temporary upgrade paths and will be removed in the next minor release, by which point every started object has migrated. Deployments skipping this release should upgrade through it. Workflow-notification delivery retries with backoff capped at ten minutes (previously five) and gives up after twelve hours of continuous failure (previously never), reporting the failure throughonError.Move agent state into the opt-in
StateLifecycle capability.State was one method doing four jobs inside
Agent— validate,
persist, broadcast, notify. It moves wholesale into aState
capability that owns storage and change ordering, so any Lifecycle
host gets durable, validated state without inheritingAgent:new State({ initialState: { count: 0 }, validateStateChange: (next, source) => validate(next, source), onChanged: (state, source) => notify(state, source), });The capability owns the
cf_agents_statestate row, lazy load with an
in-memory cache, initial-state seeding, and validated persistence. It
runs only theonStarthook (versioned schema init under its owncf_agents:state_schema_versionkey) and reaches Lifecycle only forstorage— no alarm, no request path. It never touches connections.Host-owned behavior is injected, not moved:
validateStateChange
stays an overridableAgentmethod and the post-changeonChanged
hook is passed intoStateas a plain option. Synchronous and
asynchronous notification hooks are both supported.Agentkeeps itsinitialStatefield and seeds it from thestategetter (standalone
hosts passinitialStatetoStatedirectly). Broadcast and the
notification hook stay onAgent, and theonMessagestate branch
stays too; only its inner write delegates to the capability.Stateis the only owner ofcf_agents_state: it creates the table,
holds the state row, and runs the legacycf_state_was_changed
cleanup in its own versioned migration.Agentused to keep its
global schema version as a row in that table; it now lives under thecf_agents:schema_versionKV key like every other capability's
version. A DO created under the old layout has the row read once,
moved to the key, and deleted on its next construction.Agent's public API and wire protocol are unchanged:state,setState(),onStateChanged, and theCF_AGENT_STATEframes behave
identically.The
WebSocketscapability now owns the Agent protocol's state sync and per-connection flags, forAgentand plain hosts alike.statetakes aStatecapability. The current value is pushed to each new connection after identity, a client'scf_agent_stateframe goes through the host'svalidateStateChange(a readonly connection is refused, a rejected change gets a genericcf_agent_state_error), andbroadcastState(source)pushes a change to every protocol-enabled connection but its source — wire it to theState'sonChanged. SouseAgent().stateandsetState()work against a plain Durable Object.protocolandreadonlyare per-connection policies decided at accept time;sendIdentity(),sendState(),applyStateFrame(),isReadonly()/setReadonly(), andisProtocolEnabled()/setProtocolEnabled()are the capability's surface for hosts that drive the sequence themselves. Theidentityoption from the previous changeset is replaced byprotocol.The readonly and no-protocol flags, their
_cf_storage in connection state, and the wrapper that hides them fromconnection.statemove out ofAgentintoagents/websockets(registerInternalConnectionKeysfor a host's own keys).Agent's public methods and wire behaviour are unchanged; it passesprotocol: falseand drives its connect sequence through the capability after its facet routing decision.
Patch Changes
Lifecycle capabilities declare how they claim traffic with
claims: "selective" | "catch-all"instead of hosts passing{ fallback: true }tolifecycle.use(). A catch-all always dispatches last, whenever it was installed. Catch-alls are unique per dispatch hook, so one foronRequestand one foronWebSocketUpgradecoexist while a second for the same hook is refused.WebSocketsdeclares itself a catch-all for upgrades and never declines one: withouthandlersit still accepts and tracks connections; handlers only add behavior on connect, message, close and error.LifecycleUseOptionsis removed.Preserve a legacy
runFiberresult when deleting its bookkeeping row fails. The cleanup error is logged and the row is left for the existing recovery pass instead of replacing the fiber body's outcome.Expose
ResumableStream.pendingCutoverIdso callers can detect whether a finished stream still awaits its cutover, and clear the pending-cutover marker only after settlement succeeds infinalizePending, keeping a failed settlement retryable instead of silently leaving the stream row live.Bump
valibotto 1.4.2, picking up the fix for a moderate-severity
advisory affectingrecord()schemas andflatten().
- 0.23.x
0.23.0
11 Sep 2026Extract facet ("sub-agent") machinery into
packages/agents/src/dynamic-agents/, add thethis.dynamicAgentscapability facade, and reposition facets as an isolation primitive rather than the recommended way to model many chat sessions.Agent's facet routing, WebSocket forwarding, virtual connections, and registry (~2,400 ofindex.ts's ~12,150 lines) move into a dedicated module registered as a Lifecycle capability (capabilityId: "dynamic-agents"); its hot paths stay composition-root wired since the capability-runner hook contract can't express request-rewrite-and-continue or post-claim WebSocket forwarding. No wire- or storage-visible identifier changes.The public surface gains
this.dynamicAgents.{get,abort,delete,has,list}plus theDynamicAgentClassandDynamicAgentStubtype names.SubAgentClassandSubAgentStubremain as compatibility aliases.subAgent()/abortSubAgent()/deleteSubAgent()/hasSubAgent()/listSubAgents()are unchanged in behavior and now delegate to the same capability —@deprecatedin place, not removed./sub/URLs,useAgent({ sub }),parentAgent(), andonBeforeSubAgentare untouched.docs/agents/sub-agents.mdis rewritten: verified workerd facet semantics (separate isolate, own SQLite, no independent alarms, bounded nesting depth, machine-pinned tree), a corrected claim about WebSocket frame forwarding (every frame wakes the root parent — it was never true that frames go directly to the child post-upgrade), and an explicit decision rule for facets vs. independent Durable Objects. Two new examples:examples/next/dynamic-agents(a supervisor running user-submitted Durable Object code as facets via Worker Loader — what facets are for) andexamples/next/chats(one top-level DO per chat plus a per-user push-based index — the recommended many-chats pattern), both with a React + Vite UI and workers-pool tests.Lifecycle owns a durable job queue, driven as an alarm event loop.
The thing in the queue is a job: a serialisable callback address — the
owning capability plus a function name — with a due time and a payload.
Capabilities and the host push jobs through the scopedjobssurface;
Lifecycle drives due jobs in timestamp order when the alarm fires, owns
dispatch retries and platform-failure deferral, arms a deadman pre-alarm
before driving so an isolate death mid-drive still wakes the object, and
derives the physical alarm purely from queue state (queue mutations re-arm
automatically; an exclusive job suppresses ordinary candidates).class Cleanup extends LifecycleCapability { async scheduleSweep(time: number) { await this.lifecycle.jobs.push({ id: "sweep", fn: "sweep", time }); } onJob({ job }: LifecycleJobContext) { // drive result: nothing = complete, { rescheduleAt } = suspend, // "yield" = leave due and wake again immediately } }The pull-based alarm-contribution model is removed: capability
getNextAlarm()/onAlarm(), hostgetNextAlarm(),LifecycleServices.alarms(rearm/disabled), andAlarmContribution
are gone. HostonAlarm()remains and runs once per alarm invocation
after due jobs are driven. Terminal application failures reach the
owner'sonJobError(), whose drive result decides advancement.The alarm memory-limit circuit breaker (#1825) moves from
Agent.alarm()
into the Lifecycle event loop, targeting the exact executing job; Agent
contributes domain policy through the newonAlarmMemoryLimit()host
hook, and Scheduler's__DO_NOT_USE_WILL_BREAK__handleAlarmMemoryLimit
escape hatch is gone. After recording a strike the breaker now finishes by
resetting the isolate withctx.abort(reason, { retryAlarm: false })
(retry of the handled alarm suppressed; the backoff alarm owns the next
wake), andAgent.destroy()uses the same no-retry abort so a completed
teardown's alarm cannot be retried into a fresh constructor that recreates
the deleted schema.Scheduler keeps its entire public API and loses its storage and due-row
loop: a schedule is one job whosefnis the callback name, and interval
schedules are single-flight jobs. Existingcf_agents_schedulesrows are
migrated into thecf_agents_jobsqueue on startup and the legacy table
is dropped. Agent's public scheduling andkeepAlive()APIs are
unchanged; its keep-alive, fiber-recovery/facet housekeeping, and
deferred-destroy wakes are now host jobs, and Think's
workflow-notification wake replaces the removed_getExtensionAlarm().Add
RoutedAgents(agents/routing), a Lifecycle capability that codifies the "user hub with one Durable Object per chat" topology: an owning Agent keeps a durable catalog of independent top-level Agents and routes to them by public ID.class UserAgent extends Agent<Env> { readonly chats = new RoutedAgents<ChatAgent, { title: string }>({ namespace: this.env.ChatAgent, route: "chats", }); constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.lifecycle.use(this.chats); } }create(),list(), andsetMetadata()never wake a target;get()returns an initialized typed stub;delete()hides the entry, condemns the target through Agent's deferred teardown, then drops the row, so a failed call is retryable and a clean teardown never surfaces as an abort error. Requests and WebSocket upgrades under/agents/user-agent/{user}/chats/{id}are forwarded to the target with the suffix preserved, and the target owns the upgraded socket, so chat frames never wake the user hub. Physical Durable Object names are opaque UUIDs held only in the catalog.create()returns the same JSON round-trip ofmetadatathatlist()does, andlist()breaks equal-timestamp ties by write order rather than by the random entry ID — derived from aMAX(seq)read of the route's own entries each write, a deliberate trade for a route sized like one owner's catalog: DO SQLite bills ~1000 writes for the cost of 1000 reads, so this is cheaper than a maintained counter row (or an index onseq, which would cost a write on every call too) until a route holds several thousand entries.Destroying the hub retries condemning every remaining entry (active or still
deleting) before its own storage is wiped — this is best-effort, not a durability guarantee: the platform wipes the hub's storage right after disposal regardless of outcome, so a target still unreachable after retries is orphaned, with no catalog row left to retry from later. That tradeoff is documented on the class and in the docs.Two documented sharp edges: pick a route that can't collide with the hub's own path segments (a coincidental match with no active entry behind it 404s instead of reaching the hub), and a routed suffix can't address a target's own dynamic agents —
Agent.fetch()resolves a/sub/{class}/{name}marker against the hub's exported classes before this capability ever sees the request, so it is served as a facet of the hub instead of being forwarded. Both are called out on the class and in the docs; the second is pinned by a regression test.examples/next/chatsis rebuilt onRoutedAgents: the hub creates, lists, searches, and deletes chats through the capability, the browser reaches each chat through the hub's route, a failedinit()handshake rolls back the catalog entry instead of leaving an ownerless chat, malformed message bodies get a400instead of an uncaught exception, and pushed activity is fenced by each message's own strictly-increasing ordinal (not a wall-clock timestamp, which can tie within a millisecond and silently discard a genuinely newer push) insideblockConcurrencyWhile, so neither a delayed push nor two concurrent ones can overwrite one that already landed.Lifecycle.use()accepts{ fallback: true }to dispatch a capability after every non-fallback one regardless of installation order.Agentinstalls its WebSockets capability as a fallback, so middleware a subclass installs from its constructor runs before the upgrade catch-all.
Minor Changes
Extract facet ("sub-agent") machinery into
packages/agents/src/dynamic-agents/, add thethis.dynamicAgentscapability facade, and reposition facets as an isolation primitive rather than the recommended way to model many chat sessions.Agent's facet routing, WebSocket forwarding, virtual connections, and registry (~2,400 ofindex.ts's ~12,150 lines) move into a dedicated module registered as a Lifecycle capability (capabilityId: "dynamic-agents"); its hot paths stay composition-root wired since the capability-runner hook contract can't express request-rewrite-and-continue or post-claim WebSocket forwarding. No wire- or storage-visible identifier changes.The public surface gains
this.dynamicAgents.{get,abort,delete,has,list}plus theDynamicAgentClassandDynamicAgentStubtype names.SubAgentClassandSubAgentStubremain as compatibility aliases.subAgent()/abortSubAgent()/deleteSubAgent()/hasSubAgent()/listSubAgents()are unchanged in behavior and now delegate to the same capability —@deprecatedin place, not removed./sub/URLs,useAgent({ sub }),parentAgent(), andonBeforeSubAgentare untouched.docs/agents/sub-agents.mdis rewritten: verified workerd facet semantics (separate isolate, own SQLite, no independent alarms, bounded nesting depth, machine-pinned tree), a corrected claim about WebSocket frame forwarding (every frame wakes the root parent — it was never true that frames go directly to the child post-upgrade), and an explicit decision rule for facets vs. independent Durable Objects. Two new examples:examples/next/dynamic-agents(a supervisor running user-submitted Durable Object code as facets via Worker Loader — what facets are for) andexamples/next/chats(one top-level DO per chat plus a per-user push-based index — the recommended many-chats pattern), both with a React + Vite UI and workers-pool tests.Lifecycle owns a durable job queue, driven as an alarm event loop.
The thing in the queue is a job: a serialisable callback address — the
owning capability plus a function name — with a due time and a payload.
Capabilities and the host push jobs through the scopedjobssurface;
Lifecycle drives due jobs in timestamp order when the alarm fires, owns
dispatch retries and platform-failure deferral, arms a deadman pre-alarm
before driving so an isolate death mid-drive still wakes the object, and
derives the physical alarm purely from queue state (queue mutations re-arm
automatically; an exclusive job suppresses ordinary candidates).class Cleanup extends LifecycleCapability { async scheduleSweep(time: number) { await this.lifecycle.jobs.push({ id: "sweep", fn: "sweep", time }); } onJob({ job }: LifecycleJobContext) { // drive result: nothing = complete, { rescheduleAt } = suspend, // "yield" = leave due and wake again immediately } }The pull-based alarm-contribution model is removed: capability
getNextAlarm()/onAlarm(), hostgetNextAlarm(),LifecycleServices.alarms(rearm/disabled), andAlarmContribution
are gone. HostonAlarm()remains and runs once per alarm invocation
after due jobs are driven. Terminal application failures reach the
owner'sonJobError(), whose drive result decides advancement.The alarm memory-limit circuit breaker (#1825) moves from
Agent.alarm()
into the Lifecycle event loop, targeting the exact executing job; Agent
contributes domain policy through the newonAlarmMemoryLimit()host
hook, and Scheduler's__DO_NOT_USE_WILL_BREAK__handleAlarmMemoryLimit
escape hatch is gone. After recording a strike the breaker now finishes by
resetting the isolate withctx.abort(reason, { retryAlarm: false })
(retry of the handled alarm suppressed; the backoff alarm owns the next
wake), andAgent.destroy()uses the same no-retry abort so a completed
teardown's alarm cannot be retried into a fresh constructor that recreates
the deleted schema.Scheduler keeps its entire public API and loses its storage and due-row
loop: a schedule is one job whosefnis the callback name, and interval
schedules are single-flight jobs. Existingcf_agents_schedulesrows are
migrated into thecf_agents_jobsqueue on startup and the legacy table
is dropped. Agent's public scheduling andkeepAlive()APIs are
unchanged; its keep-alive, fiber-recovery/facet housekeeping, and
deferred-destroy wakes are now host jobs, and Think's
workflow-notification wake replaces the removed_getExtensionAlarm().Add
RoutedAgents(agents/routing), a Lifecycle capability that codifies the "user hub with one Durable Object per chat" topology: an owning Agent keeps a durable catalog of independent top-level Agents and routes to them by public ID.class UserAgent extends Agent<Env> { readonly chats = new RoutedAgents<ChatAgent, { title: string }>({ namespace: this.env.ChatAgent, route: "chats", }); constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.lifecycle.use(this.chats); } }create(),list(), andsetMetadata()never wake a target;get()returns an initialized typed stub;delete()hides the entry, condemns the target through Agent's deferred teardown, then drops the row, so a failed call is retryable and a clean teardown never surfaces as an abort error. Requests and WebSocket upgrades under/agents/user-agent/{user}/chats/{id}are forwarded to the target with the suffix preserved, and the target owns the upgraded socket, so chat frames never wake the user hub. Physical Durable Object names are opaque UUIDs held only in the catalog.create()returns the same JSON round-trip ofmetadatathatlist()does, andlist()breaks equal-timestamp ties by write order rather than by the random entry ID — derived from aMAX(seq)read of the route's own entries each write, a deliberate trade for a route sized like one owner's catalog: DO SQLite bills ~1000 writes for the cost of 1000 reads, so this is cheaper than a maintained counter row (or an index onseq, which would cost a write on every call too) until a route holds several thousand entries.Destroying the hub retries condemning every remaining entry (active or still
deleting) before its own storage is wiped — this is best-effort, not a durability guarantee: the platform wipes the hub's storage right after disposal regardless of outcome, so a target still unreachable after retries is orphaned, with no catalog row left to retry from later. That tradeoff is documented on the class and in the docs.Two documented sharp edges: pick a route that can't collide with the hub's own path segments (a coincidental match with no active entry behind it 404s instead of reaching the hub), and a routed suffix can't address a target's own dynamic agents —
Agent.fetch()resolves a/sub/{class}/{name}marker against the hub's exported classes before this capability ever sees the request, so it is served as a facet of the hub instead of being forwarded. Both are called out on the class and in the docs; the second is pinned by a regression test.examples/next/chatsis rebuilt onRoutedAgents: the hub creates, lists, searches, and deletes chats through the capability, the browser reaches each chat through the hub's route, a failedinit()handshake rolls back the catalog entry instead of leaving an ownerless chat, malformed message bodies get a400instead of an uncaught exception, and pushed activity is fenced by each message's own strictly-increasing ordinal (not a wall-clock timestamp, which can tie within a millisecond and silently discard a genuinely newer push) insideblockConcurrencyWhile, so neither a delayed push nor two concurrent ones can overwrite one that already landed.Lifecycle.use()accepts{ fallback: true }to dispatch a capability after every non-fallback one regardless of installation order.Agentinstalls its WebSockets capability as a fallback, so middleware a subclass installs from its constructor runs before the upgrade catch-all.Add the experimental
agents/sessionsLifecycle capability and theagents/contextmodule.Sessions owns durable conversation storage: a tree of messages with branches and compaction overlays, streamed and byte-budgeted reads, and full-text search whose index is built by the first
search()call. Every table isWITHOUT ROWIDwith no secondary index, so a text append bills one row on an object that has never searched.Sessions stores MESSAGES; it is not a file store. A message rides in one SQLite row until its serialized JSON exceeds the 1.5 MiB row budget, and a message larger than that is split across continuation rows in
cf_agents_session_message_chunksand reassembled on read. Nothing is truncated and nothing is too large to store, so there is no size error to catch and nothing to configure. Slices are cut on UTF-8 byte boundaries and never inside a surrogate pair.Splitting is not a way to shrink the database: continuation rows live in the same Durable Object as the message, inside the same 10 GB. Sessions imposes no upper bound on a single message, so
appendMessage(msg, { source: "client" })sanitizes and strips reserved metadata but does not limit size; bounding untrusted input is the application's job. An application that handles files should keep them in a file store and put a reference in the message, as Think does with its Workspace.A byte budget bounds hydrated memory rather than the first slice:
getRecentHistory()charges each row its full stored size, continuation rows and attachments included.Prompt context moves out of conversation storage into
agents/context:ContextBlocks, the frozen system prompt,AgentContextProvider,AgentSearchProvider, and the skill providers. TheSessionhandle stores messages and nothing else.Breaking: the experimental memory stack is removed. The
agents/experimental/memory/sessionandagents/experimental/memory/utilssubpaths no longer exist, takingSession.create(),SessionManager,PostgresSessionProvider,PostgresContextProvider,PostgresSearchProvider,R2SkillProvider, and theSessionProviderinterface with them. Replacements:Session.create(this).withContext(...)→ installnew Sessions()on the Lifecycle and declare blocks withnew ContextBlocks([...])fromagents/context.createCompactFunction,truncateOlderMessages, and the token estimators →agents/sessionsandagents/chat.AgentSearchProvider,AgentContextProvider→agents/context.SessionManager→ oneSessionscapability holds many sessions by id; a user-facing conversation directory belongs to a parent or router Durable Object.Postgres providers have no replacement; Sessions is Durable Object SQLite only.
Legacy
assistant_*tables are lifted and dropped. On the first wake of a Sessions-backed object,assistant_messagesandassistant_compactionsare copied in SQL, verified row by row, and dropped;assistant_sessionsandassistant_ftsare dropped. A source whose rows do not all verify is left in place with asession:migration:incompleteevent and the schema version is not stamped, so the lift retries on a later start. There are no tombstone copies, so rolling back after a migration loses that object's conversation.Also add Computer and legacy Shell projection to
SkillRegistryso Agent Skills can be read and edited as workspace files without making Workspace own conversation data.streams: rollover block log and an atomic stream → message cutover; no more stream-buffer sweeps.
The Streams chunk log is now mutable rollover blocks: an append grows the open block row (an UPDATE) until it reaches 256 KB, then opens the next. Same one billed row per append as before, but a stream of thousands of chunks is a handful of rows to delete instead of thousands. Existing
cf_agents_stream_chunksrows are folded into blocks lazily, one stream at a time on first touch, so startup never reads the whole legacy log; the table is dropped once it is empty.writer.close({ commit, discard })(anderror(reason, { … })) settles the stream, runs the caller's synchronous writes and deletes the stream's rows in one SQLite transaction.Session.__DO_NOT_USE_WILL_BREAK__sync().upsert()is the matching synchronous message write; itsafter()dispatches the change feed and auto-compaction once the transaction commits.Chat hosts (
AIChatAgent,Think) now persist the finished turn's assistant message inside that cutover: the message, the stream's settlement and the deletion of its temporary rows commit together, so a crash leaves either the live stream (recovery rebuilds the message from it) or the message, never neither.ResumableStream.start()reclaims anything a crash left behind. The_cleanupStreamBuffersalarm is no longer armed (cleanupStreamBuffersandSTREAM_CLEANUP_DELAY_SECONDSare removed fromagents/chat; the host callback is kept as a no-op so alarms persisted by earlier versions still resolve).Make the alarm memory-limit circuit breaker (#1825) a self-contained
Lifecycle concern instead of an Agent-mediated one.Recovery-loop membership is now a property of the job row
(LifecycleJobPushOptions.recoveryLoop): flagged jobs are backed off by
the breaker on a strike and purged when it seals at the strike budget,
without disturbing unrelated rows — a recovery schedule can no longer
silently escape the breaker. The publicScheduleOptionsvocabulary is
unchanged: schedules only shape future work, and chat recovery reaches the
flag through internal scaffolding (RecoveryLoopScheduleOptions) retained
only for legacy rows and routed dynamic agents. Root recovery moves to Tasks;
the scaffolding can be deleted when Tasks supports routed child wakes.
Capabilities can react to a strike through the new optionalonMemoryLimit
hook, hosts throughonAlarmMemoryLimit, and the context identifies the job
that was executing when one exists. The strike budget is real Lifecycle
configuration (Lifecycle.install(host, { maxAlarmMemoryLimitStrikes }))
rather than a composition-root side channel. Until Tasks supports routed
child wakes, a sealed recovery schedule also forwards the seal to its owning
dynamic agent so a chat child under a plain Agent root persists its exhausted
incident and terminal notification.Removed accordingly:
Agent.onAlarmMemoryLimit's policy relay, the_cf_recoveryAlarmCallbackstemplate hook,Scheduler.applyMemoryLimitPolicy,
andsetLifecycleAlarmMemoryLimitStrikes.AIChatAgentandThinkflag their
routed recovery fallback viachatRecoverySchedulePolicyand seal in-flight
incidents from their own protectedonAlarmMemoryLimithooks; both now
requireagents >= 0.23.0from the pending release batch (they consume its
newagents/chatrecovery exports and no longer implement the old
template-method breaker hooks). Agent retains a
sealed-only call to_cf_sealMemoryLimitedRecoveryso already-published chat
packages whose peer ranges accept agents 0.23 keep terminal notifications;
that fallback carries no callback-name or queue policy.Move Voice and Channels into explicit Agents subpath exports.
Voice is available from
agents/voicewith isolatedtypes,client,react,errors,workers-ai,sfu, andtextentries. Channels is available fromagents/channelswith separate email, Slack, Telegram, browser Voice, AI SDK,
and TanStack AI adapters.Channels includes streamed outbound delivery through
ChannelHost.stream(),
provider-native Slack and Telegram streaming, fallback and fanout stream
handling, and AI SDK stream conversion.Move WebSockets out of Lifecycle into the opt-in
WebSockets
capability, with callables served from anRpcTarget.Lifecycle no longer models WebSockets — many hosts never use sockets.
Hosts that want connections install the capability, which owns the
subsystem end to end:new WebSockets({ handlers: { onConnect, onMessage, onClose }, callables: new RoomCallables(), });The capability claims WebSocket upgrades, accepts hibernating sockets,
dispatches handlers inside the host invocation boundary, reciprocates
close handshakes, closes owned connections on host destruction, and
answersgetConnections()/getConnection(). Without it installed,
upgrades are declined.callablesexposes anRpcTarget's prototype methods to remote
callers over a Cap'n Web session (?__agents_rpc=capnweb), with nativeReadableStreamstreaming.Agentadds no new surface for this: its@callable()-decorated methods are its interface, served on every wire
— natively over the legacy JSON RPC protocol and, through the
decorator-derived target, over the Cap'n Web endpoint. There is no
separate browser client either:useAgent().stub/callreach the
same interface, and a plain host's endpoint is onenewWebSocketRpcSession(new WebSocket(callablesRpcUrl(url)))away.Agentinstalls the capability itself, so itsonConnect/onMessage/onClose/onError/getConnectionTagsoverrides and connection APIs
behave exactly as before (same wire, same hibernation attachment
format). The Lifecycle host contract drops the WebSocket hooks and
Lifecycle'sgetConnections/getConnection/broadcastare removed.Lifecycle keeps only generic platform pass-throughs
onWebSocketUpgradeplusonWebSocketMessage/Close/Errorfor
capability-owned hibernation wakes — andLifecycleServicesgains a
narrowsocketssurface (accept/get) and a connection/request scope onrunInHostContext. The capability interaction contract (three
channels: hooks, services, composition-root apertures) is now
documented onDurableObjectCapability.
Patch Changes
Define the Lifecycle job dispatch contract. Job ids are now scoped to their
owning capability: a cross-owner id collision throws instead of silently
replacing the other owner's job. A same-idpush()orreschedule()made
while a job is dispatching supersedes the returned drive result, so a wake
pushed mid-drive can no longer be lost — and each due job is refetched
before dispatch, so a job replaced earlier in the same alarm cycle is
skipped instead of dispatched from its stale snapshot. A dispatch that
outlives its job's
hung timeout logs a warning and emitsjob:slow_dispatchtelemetryonJobmust stay bounded and detach unbounded work.browser_executeno longer sends the durablecallslog to the model. The persisted tool part keeps it for UIs and audit, matching the Code Mode tool's own projection.Run root-agent chat recovery continuations as chained Tasks instead of schedule rows. Initial recovery attempts deduplicate by incident, delayed retries use durable Task sleeps, and platform failures replay through Task claims. AI Chat and Think share one reserved recovery definition and preserve their existing bounded callback handoff behavior: a failure before handoff stays with the current queue execution, while a detached post-handoff platform failure enqueues exactly one replacement.
Tasks now propagate condemned-isolate failures out of journaled steps and apply alarm memory-limit backoff and sealing to the run whose wake struck — claim stripped and deadline pushed, so startup reconciliation cannot resurrect it and the reclaim still sees an interrupted attempt. Task wake jobs are pushed with a single dispatch attempt so a platform failure rejects the alarm instead of being retried into a silent reschedule of the still-claimed run. Lifecycle gains
trackAlarmWork(): work a job hands off at a bounded return stays inside that alarm's memory-limit breaker domain after the alarm returns, so other jobs stay live while a memory reset from the handoff still records a strike — one strike per reset however many flows observe it — and strikes clear only once no handed-off work is outstanding and the last of it settled clean.retain: falsenow removes failed and cancelled runs as well as completed runs, releasing journals and idempotency keys after every terminal outcome. Routed dynamic agents temporarily retain the root-owned schedule transport until Tasks supports routed child wakes.AI Chat and Think require
agents >=0.23.0, the pending release batch containing the shared recovery Task definition and internal enqueue support.Replatform chat's resumable streams onto the
agents/streamscapability.ResumableStreamis now a thin adapter overStreams: chat's in-flight turn output lives in the shared durable chunk log (cf_agents_streams/cf_agents_stream_chunks), packed ~10 wire chunks per stored segment for write economy, with completion/error mapped onto stream settlement and retention keyed off the stream row'supdated_at(sweeps no longer scan the chunk table). Existingcf_ai_chat_stream_*tables migrate wholesale — including an in-flight stream — on first construction after upgrade, then are dropped.AIChatAgentandThinkexpose the backing capability asreadonly streams, so anystreams.read()consumer on the same Durable Object can observe chat streams. The chat wire protocol, replay handshake, and recovery behavior are unchanged.Compaction summaries now serialize structured tool outputs as JSON instead of
[object Object], matching how tool inputs were already rendered. Fixes #2138.Cut storage row writes across Streams, the chat adapter, and Tasks — the streaming hot path now writes exactly what the pre-capability chat pattern wrote.
Streams: the append fence is a read instead of a guarded UPDATE (a Durable Object executes one synchronous block at a time, so state-check + tail-read + INSERT is exactly as atomic), removing one stream-row write per append. The stream row is written only at open and settle; settlement stamps the final cursor, and live cursors/liveness derive from the chunk log's tail.
readBatchestermination and the reader liveness checks moved to narrow reads.Chat adapter: the retention sweep decides abandonment in two phases (coarse row cutoff, then one indexed chunk-tail read per candidate) so an actively appending stream is never swept; the legacy migration imports rows complete (final count and last-activity stamped up front, chunk imports are bare INSERTs — 1+N writes instead of 1+2N);
destroy()no longer flushes chunks it deletes in the same call; the cleanup alarm no longer scans the table twice; dead_segmentIndexstate removed.Tasks: claim refreshes amortize to one row write per half claim-slack of wall time instead of one per step; already-elapsed sleeps journal born-completed in one INSERT; duplicate status messages skip their write; startup reconcile skips job-queue upserts that already match; a parked-run cancel settles in one row write; settle paths only re-sync the wake mirror when their write actually landed.
Replay memory is bounded: the chat adapter's chunk replay iterates the stored log in pages (a generator over paged reads) instead of materializing the whole turn per reconnecting client.
Schema: the hot-write capability tables (stream chunks, task runs, task steps, jobs — none released) are now WITHOUT ROWID. Cloudflare bills index maintenance as rows written, and an ordinary rowid table's PRIMARY KEY is a hidden UNIQUE index — so every chunk append was billing 2 rows despite being one table write. WITHOUT ROWID makes it exactly 1. The stream metadata table deliberately stays a rowid table: rowid is the insertion-order tiebreak that keeps newest-first deterministic for same-millisecond rows, at one billed row per stream open. The task runs table also drops its
(state, next_at)index, which taxed every claim/refresh/settle write to speed one startup scan.The in-suite storage-ops benchmark now pins adapter/legacy write parity exactly (12 table rows per 100-chunk turn, ~8.5× under naive per-chunk appends), models the two-phase sweep, and a write-accounting test pins the billed model per statement (a 100-chunk turn bills 14 rows vs the legacy schema's 33).
Add
agents/streams: durable incremental output as a Lifecycle capability (experimental).One
Streamsinstance per Durable Object owns an ordered, durable chunk log per stream with a monotonic cursor:open()(idempotent on the id), synchronous durableappend()that wakes live readers,close()/error()settlement, replay-then-tailread({ from, signal })plus its batched formreadBatches({ from, signal, batchSize, onUpToDate })(arrays per replay slice and per live-tail wakeup, with a caught-up-to-tail signal), indexed non-uniquetags for find-the-latest-stream-of-an-operation lookups (open(id, { tag })/list({ tag })),sseResponse()for one-call SSE serving with nativeLast-Event-IDresume andup-to-date/done/errorcontrol events, andstatus()reporting state, cursor, and last activity. Reads are independent of producer liveness; the capability needs no alarm, so it also works on facets.Streams is the incremental-output half of the pattern the Tasks migration validated, composed without coupling: a task step appends to a stream and checkpoints
{ streamId, cursor }, and itsrecovercallback readsstreams.status()as durable interruption evidence — proven across a real SIGKILL by the e2e suite, where recovery finalizes the stream at exactly the chunks that survived. Design record:design/rfc-streams.md.Keep attachments out of the message row.
A part that declares a non-text media type and carries its bytes inline is now stored separately, addressed by its SHA-256, and put back verbatim on read. The message keeps a pointer and its
mediaType, so a round trip is exact and a message row stays small however large its payloads are. This is invisible: there is no pointer-mode read.The rule is typed rather than sized: an image is extracted at any size, and text is never extracted at any size — long prose still splits across continuation rows. The two mechanisms are independent, so media leaves before the row is measured and a message carrying a large image usually has no continuation rows at all.
Payload lifetime is derived from message references; the bytes go when the last reference does. Identical payloads store once, which makes a retried write free.
getRecentHistory()loses itsminRecentMessagesargument. The byte budget is now a hard ceiling: a message-count floor admitted rows whatever their size, so a window of media-heavy messages could hydrate far past the limit meant to bound it. The newest message is always returned.Also fixes two migration faults that could lose data:
AIChatAgentdropped its legacy table when rows were merely accounted for rather than imported, deleting any row that failed to parse; and Sessions stamped its schema version even when a legacy lift was incomplete, so it never retried. Both lifts are idempotent, so the source now survives until every row has actually landed.appendMessagereturns the same message whether it inserted or found a duplicate, and dispatches itsappendevent before any auto-compaction runs, so a cache mirror never misses the row that triggered a compaction. A change-feed listener that throws is reported through thesession:errorcapability event instead of rejecting the write it was told about.Preserve sub-agent connection state set in
onConnectfor the first client message by completing queued connection operations before the connect handler returns.chat: derive the recovery forward-progress marker from the stream log instead of bumping a KV counter per credited chunk.
ResumableStream.progressMarker()counts durably flushed segments — live streams from their log tails, deleted streams from a retired total folded in as their rows are removed — so the marker stays monotonic across cutover and reclaim, never moves on a reconnect replay or a recovery re-persist, and ignores compaction. A parent forwarding a sub-agent's output credits it explicitly throughcreditProgress(). Nothing is written per chunk any more; one row is written per stream retired. The old KV counter is read once per isolate and seeded into the marker so an in-flight incident never sees it drop, and the hosts mirror the marker's durable part back to that key per stream retired, so a rollback reads no lower either. The Streams sync aperture gains anonDeletehook so a chat row deleted through the public capability is retired like any other.AIChatAgentnow flushes a settled tool result to SQLite the moment it is stored, asThinkalready did, so it is durable before the next packed flush and counts as progress immediately. The work budget's unit is now the durable segment, andDEFAULT_CHAT_RECOVERY_MAX_WORKmoves from 1000 to 10000 to stay as generous as before for delta-heavy turns. Two cutover fixes ride along: a Think agent-tool child now keeps its stream rows for the parent to tail after completion, as ai-chat already did, and the Streams capability re-derives its legacy-table flag after a rolled-back cutover.sessions:
history({ newestFirst: true })streams the active path leaf → root by following parent pointers, paying one row per message the consumer takes; compaction overlays are planned only once the walk reaches a compacted span. The change feed now reportsimport(one per rowimportMessage()actually writes) andcompaction(an overlay stored throughaddCompaction()), so a host cache can tell when the path changed underneath it.Add
agents/tasks: durable, replayable background execution as a Lifecycle capability (experimental).One
Tasksinstance per Durable Object owns any number of named Task definitions declared in its constructor (new Tasks({ definitions: {...} }), mirroring the Scheduler's callbacks map), so the registry is rebuilt on every wake and recovery of in-flight runs is correct by construction. Runs start with the typedtasks.run(name, input, options), andtasks.handle(name)gives a typed lens scoped to one definition. A run survives process loss and deployments by replaying its handler from the top: completedstep.do()steps return journaled results,step.sleep()/step.sleepUntil()consult persisted deadlines, and execution continues from the first unfinished step under generation fencing. Steps carry per-attempt retry and timeout policy, stable idempotency keys for external deduplication, andstep.status()progress with a replay live gate that never re-publishes old progress as new.There is no separate recovery mode: an unclean interruption replays the handler on the next wake, and handlers make replay safe with step idempotency keys for external writes and durable evidence (a stream's cursor, a rows-written count) read at the top of the work. The interrupted step is first-class evidence:
step.interruptedis{ name, attempt }on a replay after process loss (nullon clean attempts), and atask:attempt:interruptedevent carries the same step. Clean step failures are not interruptions; the retry policy owns them.Agentinstalls the capability automatically as experimentalthis.tasks, with subclass definitions declared on the overridabletaskDefinitionsfield and framework-internal definitions attached through a composition-root aperture. The internal chat frameworks now run on it: Think and AIChatAgent chat turns and Think's messenger replies each execute as a journaled step withstash()persisted in host storage, and a replay whose live closure is gone branches into the unchanged ChatRecoveryEngine (and messenger recovery) on durable evidence. The legacyrunFiber()/startFiber()APIs are unchanged and still recovered by their own scan; facet-hosted turns stay on the legacy engine until routed Fibers land.Runs are durably accepted (
tasks.run()returns a receipt; idempotency keys join existing runs), inspectable (get,getByIdempotencyKey,list), cooperatively cancellable, and retained until deleted. The capability stores run deadlines in its own tables and mirrors each non-terminal run as one job in the Lifecycle work queue (never touching the physical alarm), so it composes with the Scheduler and other capabilities on one shared, queue-derived alarm. Design record:design/rfc-fibers.md(shipped under the name Tasks).
- 0.22.x
0.22.0
27 Aug 2026- removed
Remove the published
agentscommand-line binary. Its advertisedinit,dev,deploy, andmcpcommands were placeholders that printed "not implemented yet" and exited successfully. Use the documented C3 starter, Vite and Wrangler commands, and MCP APIs instead. Make durable chat recovery unconditional for
AIChatAgentandThink.Every chat turn now runs in a recovery fiber, including WebSocket, programmatic, retry, and continuation paths.
chatRecoveryacceptstrueor a configuration object;falseis no longer supported. Previously compiled JavaScript that still suppliesfalsesafely receives the default recovery configuration.To keep durable bookkeeping while preventing automatic inference after an interruption, return
{ continue: false }fromonChatRecovery(). Use durable cancellation, side-effect, or spend state in that hook and tunechatRecoverybudgets when retries must be bounded.Vendor the required PartyServer runtime into
agents/lifecycleand add a reusable Durable Object lifecycle for startup, request interception, alarms, and WebSockets.Agentnow directly extends Cloudflare'sDurableObjectand composes the same lifecycle used by standalone objects; standalone hosts use the explicitLifecycle.install(this)factory (or the expandednew ...plusinstallHandlers()form). Both Agent subclasses and standalone hosts use the existingrouteAgentRequest()API and/agentsURL prefix; the lifecycle entry point does not introduce a second public router.Lifecycle WebSockets always use Cloudflare's Hibernation API; the
static options.hibernateswitch and in-memory connection mode are removed. Named Durable Objects use nativectx.id.name, while a read-only__ps_namefallback migrates objects created by older releases without writing new compatibility state.
Minor Changes
Make durable chat recovery unconditional for
AIChatAgentandThink.Every chat turn now runs in a recovery fiber, including WebSocket, programmatic, retry, and continuation paths.
chatRecoveryacceptstrueor a configuration object;falseis no longer supported. Previously compiled JavaScript that still suppliesfalsesafely receives the default recovery configuration.To keep durable bookkeeping while preventing automatic inference after an interruption, return
{ continue: false }fromonChatRecovery(). Use durable cancellation, side-effect, or spend state in that hook and tunechatRecoverybudgets when retries must be bounded.Vendor the required PartyServer runtime into
agents/lifecycleand add a reusable Durable Object lifecycle for startup, request interception, alarms, and WebSockets.Agentnow directly extends Cloudflare'sDurableObjectand composes the same lifecycle used by standalone objects; standalone hosts use the explicitLifecycle.install(this)factory (or the expandednew ...plusinstallHandlers()form). Both Agent subclasses and standalone hosts use the existingrouteAgentRequest()API and/agentsURL prefix; the lifecycle entry point does not introduce a second public router.Lifecycle WebSockets always use Cloudflare's Hibernation API; the
static options.hibernateswitch and in-memory connection mode are removed. Named Durable Objects use nativectx.id.name, while a read-only__ps_namefallback migrates objects created by older releases without writing new compatibility state.Throttle chat UI updates by default in
useAgentChatStreaming writes chat state once per chunk, and each write re-renders. When
chunks arrive in a burst — a resumed stream replaying a long turn, for
example — React reaches its 50-render limit and throws "Maximum update depth
exceeded", which the AI SDK reports as a failed turn even though the server
completed it (#1913).useAgentChatnow coalesces those updates every 50ms, which removes about 78%
of renders on a fast stream and matches the value the AI SDK documents. The
first chunk of a stream is never delayed. Passthrottle: falseto render
every chunk as it arrives, or a number to change the interval. The deprecatedexperimental_throttleis still honoured. Message snapshots, functional
updates, and streamed continuations resolve against the current chat store, so
coalescing renders cannot roll assistant content back to an older snapshot.Add
Scheduler, a reusable Lifecycle capability for persistent delayed, dated, cron, and interval callbacks, underagents/schedules. Scheduled callbacks are registered on the Scheduler itself (new Scheduler({ callbacks: { ... } })), andset()/every()type both the callback name and the payload against that registration, so the typed scheduling surface and the runtime dispatch target are the same object.LifecycleCapabilitysupplies every capability with storage, readiness, startup state, alarm coordination, a host invocation boundary, best-effort events, and generic capability routing — Scheduler consumes only that standard surface plus its callbacks and policy options, so any host that installs it configures nothing else. Lifecycle owns the physical Durable Object alarm and routes matching capability messages between Agent facets through one internal transport aperture, preserving existing root-owned facet schedule rows without Scheduler-specific Agent RPC methods or an Agent adapter.Agentuses the same Scheduler behind its existing APIs — name-basedthis.schedule(60, "methodName")keeps dispatching to Agent methods through a composition-root resolver — and preserves callback context, observability, retries, OOM handling, and alarm behavior. MCP now receives storage from Lifecycle when installed. Explicit destruction disposes live capability resources once, then clears shared Durable Object storage withdeleteAll(). Think workflow notifications now contribute their wake time through Lifecycle instead of writing the physical alarm directly. The previousagents/scheduleparser entry point remains as a deprecated compatibility alias. Agent exposes the composition root as experimentalthis.lifecycleandthis.schedulerproperties. Theagents/lifecycleentry point and the capability surfaces built on it (Scheduler, installingMCPClientManagerdirectly as a capability) are experimental and may change between releases; Agent's established APIs are unaffected.Compatibility notes:
MCPClientManagerOptions.storageis removed — the manager receives storage from the Lifecycle it is installed on, so standalone construction with an explicitDurableObjectStorageis no longer supported. Scheduled callbacks now receive the documented parsedScheduleobject as their second argument (previously the raw storage row, whosepayloadwas an unparsed JSON string). The internal_cf_*ForFacetschedule RPC methods are replaced by the generic_cf_routeLifecyclecapability aperture; facets always run the same deployed script, so no coordination is required.- removed
Remove the published
agentscommand-line binary. Its advertisedinit,dev,deploy, andmcpcommands were placeholders that printed "not implemented yet" and exited successfully. Use the documented C3 starter, Vite and Wrangler commands, and MCP APIs instead. Make
MCPClientManagera reusable Durable Object lifecycle capability. It now owns schema initialization, persisted HTTP and RPC connection restoration, and OAuth callback interception when installed withLifecycle.use(), while preservingAgent.this.mcpand the existing Agent MCP APIs.agents/lifecycleexports theLifecycleObjecthost interface and canonicalgetCurrentAgent()accessor; Lifecycle supplies that context to host hooks while capability hooks remain self-contained.
Patch Changes
Route asynchronous callable and streaming responses through the facet WebSocket frame that originated each RPC.
Add Agents SDK instrumentation and agent instance identity attributes to SDK-created spans.
Batch replayed chunks during stream resume so long turns do not exceed React's
update limit and report a false error.Keep Session compaction overlays scoped to their selected conversation branch and preserve deterministic ordering for overlays created in the same second.
Keep sub-agent WebSocket operations routable and ordered across live and delayed contexts and before broadcasts, report routing failures, and preserve nested sub-agent broadcasts across RPC callbacks.
Fall back to no-op tracing when an older Workers runtime exposes tracing without
startActiveSpan, preventing Agent initialization from failing.
- 0.21.x
0.21.0
18 Aug 2026Expose
WebSocketChatTransportand its connection types from the framework-neutralagents/chat/transportentry point. React peers are now optional for framework-neutral clients and servers.Existing users of
agents/chat/reactor@cloudflare/ai-chat/reactmust continue to declare compatiblereactand@ai-sdk/reactdependencies explicitly.Accept AI SDK flexible schemas in
agentTool, including Valibot adapters, while preserving schema-driven input inference and structured output validation. Zod is no longer a peer requirement of@cloudflare/ai-chat.Existing custom schemas that no longer type-check as AI SDK
FlexibleSchemamust use the schema library's AI SDK adapter or wrap raw JSON Schema withjsonSchema(). Validation-only Standard Schema implementations are insufficient because tool inputs must expose JSON Schema to the model.Add connection-scoped Kitesurf support to Browser Tools through the
browser: "kitesurf"session option. Unsupported durable session, Live View, recording, pause/resume, and Kitesurf-backed Quick Action surfaces remain unavailable.Existing Browser Tools users should note:
Minor Changes
Expose
WebSocketChatTransportand its connection types from the framework-neutralagents/chat/transportentry point. React peers are now optional for framework-neutral clients and servers.Existing users of
agents/chat/reactor@cloudflare/ai-chat/reactmust continue to declare compatiblereactand@ai-sdk/reactdependencies explicitly.Accept AI SDK flexible schemas in
agentTool, including Valibot adapters, while preserving schema-driven input inference and structured output validation. Zod is no longer a peer requirement of@cloudflare/ai-chat.Existing custom schemas that no longer type-check as AI SDK
FlexibleSchemamust use the schema library's AI SDK adapter or wrap raw JSON Schema withjsonSchema(). Validation-only Standard Schema implementations are insufficient because tool inputs must expose JSON Schema to the model.Add connection-scoped Kitesurf support to Browser Tools through the
browser: "kitesurf"session option. Unsupported durable session, Live View, recording, pause/resume, and Kitesurf-backed Quick Action surfaces remain unavailable.Existing Browser Tools users should note:
Large base64 values returned outside the canonical
{ type: "browser_screenshot", mediaType, data }shape are now redacted. Return screenshots in that shape or store binary output elsewhere.TanStack browser tools have one output channel, so screenshot output is reduced to the compact model-facing summary rather than returning raw base64 data.
Pass Workflow
retentionthroughAgent.runWorkflow().
Patch Changes
Add
buildAgentPath()andbuildAgentUrl()for constructing canonical root-first Agent and sub-agent addresses for external HTTP requests, WebSocket connections, callbacks, and webhooks. React sub-agent connections now share the same descendant path encoder.Stream forwarded request bodies into sub-agents instead of buffering them in the parent Durable Object.
Agent._cf_forwardToFacetandrouteSubAgentRequestboth didforwardInit.body = await req.arrayBuffer()before dispatching to a child facet, materialising the entire request body in the parent's isolate. Two consequences:The read sat in front of application-level validation.
Agent.fetchreturns beforeonRequestwhenever the path matches/sub/{class}/{name}, so an app that carefully bounded request bodies inonRequeststill had an unbounded read ahead of it — and no way to bound it itself.The cost was per hop. A nested
/sub/.../sub/...address re-materialised the same bytes at every level.
Both call sites now pass
req.bodythrough as a stream. Measured onwrangler dev --localwith a handler that never reads the body, peak RSS across theworkerdprocesses for a single POST:Request body facet route, before facet route, after canonical route (control) 16 MB +75 MB +4 MB +2 MB 64 MB +268 MB +4 MB +2 MB 128 MB +546 MB +4 MB +2 MB This restores the behaviour from before #1443, which switched to an explicit
RequestInitin order to set a header on WebSocket upgrades and re-attached the body witharrayBuffer()as a side effect. TheUpgradeheader handling from that fix is unchanged.One behavioural note: backpressure now reaches the client. A child that returns without reading the body will cause the remainder of the upload to be cancelled, where previously the parent drained it in full. Existing handlers that require the complete upload must consume or stream
request.bodybefore returning.Send Browser Run extraction schemas under
response_format.json_schema, matching the Quick Actions/jsoncontract.Direct
browserExtract()andrunQuickAction()callers must renameresponse_format.schematoresponse_format.json_schema. The model-facingbrowser_extracttool still accepts its schema in the top-levelschemafield.Treat
useAgentChatobserver error frames as terminal responses.Plain-text error bodies are no longer parsed as stream chunks or merged into an empty assistant message. Error frames now clear observer streaming, replay, recovery, and tool-continuation state even when they omit
done, matching the transport-owned stream behavior.Existing observer UIs that displayed error bodies as assistant messages must move those diagnostics to a dedicated error surface.
Update PartyServer and the Cloudflare Workers development toolchain for
@cloudflare/workers-typesv5 compatibility. New Think projects now use Workers Types v5 with matching Wrangler and Vite plugin versions.Use
wrapAISDK()as the single AI SDK v6 and v7 tracing integration, removing the separatecreateAISDKTelemetry()callback adapter that could not preserve theinvoke_agentparent hierarchy. Mark asynchronously decided AI SDK v7 top-level approval spans when they outlive their invocation.Existing
createAISDKTelemetry()users must:Remove it from
registerTelemetry()and per-call telemetry integrations.Wrap the AI SDK namespace and call the wrapped functions instead:
import * as ai from "ai"; import { wrapAISDK } from "agents/observability/ai"; const tracedAI = wrapAISDK(ai, { storeMessages: true, storeTools: true, }); await tracedAI.generateText(/* ... */);Update telemetry queries that use the removed callback-only
cloudflare.agents.call.idorcloudflare.agents.tool_context.*attributes.
Validate BrowserConnector tool arguments before execution and reject JSON-stringified Browser Run extraction schemas with an actionable error.
Existing callers that pass values outside the documented schemas must correct them before execution:
Pass extraction schemas as JSON objects, not JSON strings.
Pass the string returned in
attachToTarget().sessionIdtocdp.send, not the complete attachment result.
Preserve spacing between streamed text segments separated by tool calls. Think messenger delivery and Voice now share the same boundary-aware text joining logic from
agents/chat.Existing users must:
Replace imports of
textDeltaFromStreamChunk()from@cloudflare/think/messengerswithTextStreamCallback, passing it the complete structured stream events.Upgrade to
agents@0.21.0when installing@cloudflare/think@0.16.0or@cloudflare/voice@0.3.6; both now requireagents >=0.20.2.Update exact-text expectations if they relied on segments around tool calls being concatenated without a space.
- 0.20.x
0.20.1
28 Jul 2026Update the MCP dependencies to stable
@modelcontextprotocol/client@2.0.0and@modelcontextprotocol/server@2.0.0, and update the retained SDK v1 compatibility dependency to@modelcontextprotocol/sdk@1.30.0. Delegate SDK-backed SSE keepalives to the upstream transports so each stream has one timer, while preserving the Agents-owned keepalive on the legacy McpAgent WebSocket bridge.Fix AI SDK v7 telemetry, which produced spans with no token counts, no finish reason, no tool results and zero durations.
Spans that must not outlive their invocation now close at the end of it rather than at the first
await. Closing at the handoff ended every WebSocket-turn span before its result existed, so every finish-time attribute was dropped. A span still open when its invocation ends is closed and markedcloudflare.agents.span.truncatedinstead of passing as complete, approval spans decided asynchronously included. A chat turn owns its own boundary rather than its caller's, so a turn that is not awaited — an ack-and-return submit, or an auto-continuation fired from a timer — is no longer cut short by the handler that started it.generateTextis bounded on the same terms asstreamText, and a turn that fails or is cancelled keeps the usage it already reported.chatandexecute_toolspans sit under theirinvoke_agentoperation span on v7, where they were previously emitted as unrelated roots, andtool_approvalsegments sit underexecute_tool.v7 has no telemetry metadata bag, so identity and turn context arrive through
runtimeContextandtelemetry.includeRuntimeContext. Reserved keys project onto the attributes v6 already emits, so a query written against v6 traces still matches v7 ones; other included keys pass through ascloudflare.agents.runtime_context.{key}, and context the caller did not mark stays off the span.
0.20.0
27 Jul 2026Add MCP SDK v2 client and server support.
MCPClientConnectionnow uses the exact-pinned@modelcontextprotocol/client@2.0.0-beta.5. It probes for stateless MCP withserver/discover, then falls back to the legacyinitializehandshake on the same connection when needed. The SDK auto-fulfills stateless elicitationinput_requiredresults through the existing form and URL elicitation handlers whilecallTool,getPrompt, andreadResourceremain pending. OAuth reauthorization discards redirect-scoped discovery after token issuance and preserves discovery-triggered authentication, allowing a changed authorization server to be rediscovered and registered without reusing the prior issuer's credentials. Legacy pushed elicitation, Streamable HTTP, SSE, RPC, OAuth, and hibernation recovery remain supported. Codemode's MCP connector now uses an SDK-neutral structural boundary compatible with both MCP client generations.Add MCP SDK v2 support to
createMcpHandler. Pass a factory returningMcpServerorServerfrom the exact-pinned@modelcontextprotocol/server@2.0.0-beta.5peer dependency to serve stateless MCP with legacy compatibility by default. The newagents/mcp/serverentry exports the stateless Agents handler without retainingMcpAgent,WorkerTransport, MCP client transports, PartyServer, or SDK v1 modules. The returned handler remains callable for Worker dispatch and exposes the lower-level SDKfetch(request, options?)method plus typednotifymethods; upstreamcloseand event-bus internals are not part of the Agents surface. The retained v1 server APIs use the exact-pinned@modelcontextprotocol/sdk@1.29.0peer dependency.The legacy compatibility fallback now uses SDK v2's web-standard transport, including fail-fast handling for unsupported server-to-client requests, active-request teardown, and the same 25-second Cloudflare SSE keepalive previously supplied by
WorkerTransport. It returns405for session-only GET and DELETE requests without constructing an application server.createLegacyMcpHandlerremains an explicit public API for SDK v1 servers and complete WorkerTransport options.The MCP client storage codec now preserves stateless discovery data with resumed HTTP sessions and preserves the binding name and props required to restore RPC servers. Stored HTTP session IDs from older Agents versions have no associated protocol version. The upgraded client discards those IDs and reconnects instead of sending an unsafe resumed request, so in-flight work tied to an old remote session does not resume.
The v2 callable handler maps verified provider-issued metadata from compatible
@cloudflare/workers-oauth-providerreleases to standard MCPAuthInfowhile preservinggetMcpAuthContext().props.The Workers handler rejects malformed, opaque, and non-HTTP browser Origins. Its default allowlist includes localhost-class Origins, the endpoint's
workers.devhostname, and a concretecorsOptions.originhostname. It applies matching Host checks to localhost andworkers.devendpoints. Custom-domain deployments with wildcard CORS can setallowedHostnamesandallowedOriginHostnamesexplicitly, or setallowedOriginHostnames: "*"when trusted upstream middleware already enforces the required Origin policy. Requests without Origin remain valid for non-browser MCP clients. Default CORS preflights allow the statelessMcp-MethodandMcp-Namerequest headers.@cloudflare/codemodeis now an optional peer. Applications that importagents/skillsoragents/browserinstall Codemode explicitly; MCP-only applications no longer install it transitively.Deprecations in this release:
experimental_createMcpHandlerwas already deprecated and remains scheduled for removal in the next major release. Its warning now directs users to an SDK v2 factory first and namescreateLegacyMcpHandleronly as a temporary bridge for sessionful SDK v1 behavior.Recycle reusable Browser Run sessions after the platform reports HTTP 410 for an expired session.
- 0.19.x
0.19.0
23 Jul 2026Reading the note…
- 0.18.x
0.18.0
22 Jul 2026Reading the note…
- 0.17.x
0.17.4
13 Jul 2026Reading the note…
0.17.3
30 Jun 2026Reading the note…
0.17.1
28 Jun 2026Reading the note…
0.17.0
26 Jun 2026Reading the note…
- 0.16.x
0.16.2
17 Jun 2026Reading the note…
0.16.1
16 Jun 2026Reading the note…
0.16.0
12 Jun 2026Reading the note…
- 0.15.x
0.15.0
8 Jun 2026Reading the note…
- 0.14.x
0.14.5
7 Jun 2026Reading the note…
0.14.4
7 Jun 2026Reading the note…
0.14.3
5 Jun 2026Reading the note…
0.14.2
5 Jun 2026Reading the note…
0.14.1
3 Jun 2026Reading the note…
0.14.0
2 Jun 2026Reading the note…
- 0.13.x
0.13.3
26 May 2026Reading the note…
0.13.2
21 May 2026Reading the note…
0.13.1
20 May 2026Reading the note…
0.13.0
19 May 2026Reading the note…
- 0.12.x
0.12.4
13 May 2026Reading the note…
0.12.3
2 May 2026Reading the note…
0.12.2
1 May 2026Reading the note…
0.12.1
1 May 2026Reading the note…
0.12.0
30 Apr 2026Reading the note…
- 0.11.x
0.11.9
29 Apr 2026Reading the note…
0.11.8
28 Apr 2026Reading the note…
0.11.7
28 Apr 2026Reading the note…
0.11.6
27 Apr 2026Reading the note…
0.11.5
23 Apr 2026Reading the note…
0.11.4
18 Apr 2026Reading the note…
0.11.3
18 Apr 2026Reading the note…
0.11.2
17 Apr 2026Reading the note…
0.11.1
16 Apr 2026Reading the note…
0.11.0
14 Apr 2026Reading the note…
- 0.10.x
0.10.2
13 Apr 2026Reading the note…
0.10.1
11 Apr 2026Reading the note…
0.10.0
7 Apr 2026Reading the note…