Releases: cloudflare/agents
agents@0.11.5
Patch Changes
-
#1353
f834c81Thanks @threepointone! - AlignAIChatAgentgenerics and types with@cloudflare/think, plus a reference example for multi-session chat built on the sub-agent routing primitive.- New
Propsgeneric:AIChatAgent<Env, State, Props>extendingAgent<Env, State, Props>. Subclasses now get properly typedthis.ctx.props. - Shared lifecycle types:
ChatResponseResult,ChatRecoveryContext,ChatRecoveryOptions,SaveMessagesResult, andMessageConcurrencynow live inagents/chatand are re-exported by both@cloudflare/ai-chatand@cloudflare/think. No behavior change; one place to edit when shapes evolve. ChatMessagestays the public message type: the package continues to exportChatMessage, and the public API/docs keep using that name.messagesstays a public field:messages: ChatMessage[].
The full stance (AIChatAgent is first-class, production-ready, and continuing to get features; shared infrastructure should land in
agents/chatwhere both classes benefit) is captured indesign/rfc-ai-chat-maintenance.md.A new example,
examples/multi-ai-chat, demonstrates the multi-session pattern end-to-end on top of the sub-agent routing primitive: anInboxAgent owns the chat list + shared memory; each chat is anAIChatAgentfacet (this.subAgent(Chat, id)). The client addresses the active chat viauseAgent({ sub: [{ agent: "Chat", name: chatId }] })— no separate DO binding, no custom routing on the server.Inbox.onBeforeSubAgentgates withhasSubAgentas a strict registry, andChatreaches its parent viathis.parentAgent(Inbox). - New
-
#1348
0693a5fThanks @threepointone! - Bump dependencies. -
#1362
d901804Thanks @threepointone! - fix(mcp): capture tool title in MCP client -
#1355
df2023fThanks @threepointone! - External addressability for sub-agents.Clients can now reach a facet (a child DO created by
Agent#subAgent()) directly via a nested URL:/agents/{parent-class}/{parent-name}/sub/{child-class}/{child-name}[/...]New public APIs (all
@experimental):routeSubAgentRequest(req, parent, options?)— sub-agent analog ofrouteAgentRequest. For custom-routing setups where the outer URL doesn't match the default/agents/...shape.getSubAgentByName(parent, Cls, name)— sub-agent analog ofgetAgentByName. Returns a typed Proxy that round-trips typed RPC calls through the parent. RPC-only (no.fetch()); userouteSubAgentRequestfor external HTTP/WS.parseSubAgentPath(url, options?)— public URL parser used internally by the routers.SUB_PREFIX— the"sub"separator constant (not configurable; exposed for symbolic URL building).
New public on
Agent:onBeforeSubAgent(req, { className, name })— overridable middleware hook, mirrorsonBeforeConnect/onBeforeRequest. ReturnsRequest | Response | voidfor short-circuit responses, request mutation, or passthrough. Default: void.parentPath/selfPath— root-first{ className, name }ancestor chains, populated at facet init time. Inductive across recursive nesting.hasSubAgent(ClsOrName, name)/listSubAgents(ClsOrName?)— parent-side introspection backed by an auto-maintained SQLite registry written bysubAgent()/deleteSubAgent(). Both accept either the class constructor or a CamelCase class name string.
New public on
useAgent(React):sub?: Array<{ agent, name }>— flat root-first chain addressing a descendant facet. The hook's.agent/.namereport the leaf identity;.pathexposes the full chain.
Breaking changes: none.
routeAgentRequestbehavior is unchanged when URLs don't contain/sub/.onBeforeSubAgentdefaults to permissive (forward unchanged).useAgentwithoutsubis unchanged.subAgent()/deleteSubAgent()gain registry side effects but preserve return types and failure modes. The_cf_initAsFacetsignature gained an optionalparentPathparameter.deleteSubAgent()is now idempotent — calling it for a never-spawned or already-deleted child no longer throws. Sub-agent class names equal to"Sub"are rejected (the/sub/URL separator is reserved).See
design/rfc-sub-agent-routing.mdfor the full rationale, design decisions, and edge cases. The spike atpackages/agents/src/tests/spike-sub-agent-routing.test.tsdocuments the three candidate approaches considered for cross-DO stub passthrough and why the per-call bridge won. -
#1346
a78bb2aThanks @threepointone! - Remove unuseddependencies,devDependencies, andpeerDependenciesfrom theagentspackage.dependencies: dropjson-schema,json-schema-to-typescript, andpicomatch. None are imported by the package;picomatchwas already pulled in transitively via@rolldown/plugin-babel.devDependencies: drop@ai-sdk/openai(only referenced in a commented-out line) and@cloudflare/workers-oauth-provider(not referenced anywhere).peerDependencies/peerDependenciesMeta: drop@ai-sdk/reactandviem.@ai-sdk/reactis already a peer of@cloudflare/ai-chat(itself an optional peer here), andviemis a regular dependency of@x402/evm, so both are supplied transitively when the relevant optional features are used.
@cloudflare/think@0.4.0
Minor Changes
-
#1350
3a1140fThanks @threepointone! - AlignThinkgenerics withAgent/AIChatAgent.Thinkis nowThink<Env, State, Props>and extendsAgent<Env, State, Props>, so subclasses get properly typedthis.state,this.setState(),initialState, andthis.ctx.props. The previousConfigclass generic is removed.configure()andgetConfig()remain, but the config type is now specified at the call site via a method-level generic:// Before export class MyAgent extends Think<Env, MyConfig> { getModel() { const tier = this.getConfig()?.modelTier ?? "fast"; // ... } } // After export class MyAgent extends Think<Env> { getModel() { const tier = this.getConfig<MyConfig>()?.modelTier ?? "fast"; // ... } }
This is a breaking change for anyone using the second type parameter of
Think. Update the class declaration and any directconfigure(...)/getConfig()call sites that relied on the class-levelConfigtype.
@cloudflare/ai-chat@0.5.1
Patch Changes
-
#1368
2fe85cbThanks @threepointone! - AddisToolContinuation: booleantouseAgentChat()so consumers can disambiguate a fresh user-initiatedstatus === "submitted"from one driven by a server-pushed tool continuation. See #1365.statusalready tracks the whole tool round-trip (submitted→streaming→ready) afteraddToolOutput/addToolApprovalResponse, on purpose — that's what #1157 asked for and what many loading-spinner UIs now rely on. But some consumers want a typing indicator only for new user messages, not for mid-turn continuations, and previously had to inspect message history to tell them apart.isToolContinuationistruefrom the momentaddToolOutput/addToolApprovalResponsekicks off an auto-continuation until the continuation stream closes (or is aborted bystop()). It isfalseotherwise — including during cross-tab server broadcasts, which surface viaisServerStreamingonly.const { status, isStreaming, isToolContinuation } = useAgentChat({ ... }); const isLoading = isStreaming || status === "submitted"; const showTypingIndicator = status === "submitted" && !isToolContinuation;
Purely additive — no change to
status,isServerStreaming, orisStreamingsemantics. -
#1366
53600d0Thanks @threepointone! - FixuseAgentChat()going silent while anonToolCallhandler is running. The server'sstreamTextends the stream as soon as the model emits a client-tool call, which droppedstatusback toreadyandisStreaming/isServerStreamingtofalsefor the full duration of the client-sidetool.execute()— often afetchtaking several seconds. Consumers had no single flag that covered the whole "turn in progress" window. See #1365.useAgentChat()now treats any unresolved client-side tool call on the latest assistant message as an active server-driven phase:isServerStreamingistruefrom the moment the tool part appears ininput-available(with an active handler —onToolCallor a deprecatedtoolsentry withexecute) until it transitions out viaaddToolOutput/addToolResult.isStreaming(status === "streaming" || isServerStreaming) staystrueacross the whole tool round-trip, including the gap between the model emitting the call and the server pushing its continuation.statusis untouched — it still means "user-initiated submission awaiting a response." Tools waiting for explicit user confirmation are excluded from the busy signal (nothing is happening until the user acts).
Consumer code simplifies to:
const { isStreaming, status } = useAgentChat({ ... }); const isLoading = isStreaming || status === "submitted"; const showTypingIndicator = status === "submitted";
No API changes. Existing code that only looked at
statusbehaves the same.
@cloudflare/ai-chat@0.5.0
Minor Changes
-
#1353
f834c81Thanks @threepointone! - AlignAIChatAgentgenerics and types with@cloudflare/think, plus a reference example for multi-session chat built on the sub-agent routing primitive.- New
Propsgeneric:AIChatAgent<Env, State, Props>extendingAgent<Env, State, Props>. Subclasses now get properly typedthis.ctx.props. - Shared lifecycle types:
ChatResponseResult,ChatRecoveryContext,ChatRecoveryOptions,SaveMessagesResult, andMessageConcurrencynow live inagents/chatand are re-exported by both@cloudflare/ai-chatand@cloudflare/think. No behavior change; one place to edit when shapes evolve. ChatMessagestays the public message type: the package continues to exportChatMessage, and the public API/docs keep using that name.messagesstays a public field:messages: ChatMessage[].
The full stance (AIChatAgent is first-class, production-ready, and continuing to get features; shared infrastructure should land in
agents/chatwhere both classes benefit) is captured indesign/rfc-ai-chat-maintenance.md.A new example,
examples/multi-ai-chat, demonstrates the multi-session pattern end-to-end on top of the sub-agent routing primitive: anInboxAgent owns the chat list + shared memory; each chat is anAIChatAgentfacet (this.subAgent(Chat, id)). The client addresses the active chat viauseAgent({ sub: [{ agent: "Chat", name: chatId }] })— no separate DO binding, no custom routing on the server.Inbox.onBeforeSubAgentgates withhasSubAgentas a strict registry, andChatreaches its parent viathis.parentAgent(Inbox). - New
Patch Changes
-
#1358
ea229b1Thanks @threepointone! - FixuseAgentChat()crashing on first render whenagent.getHttpUrl()returns an empty string. This happened in setups where the WebSocket handshake hadn't completed by the time React rendered — most commonly when the agent is reached through a proxy or custom-routed worker — because@cloudflare/ai-chatunconditionally callednew URL(agent.getHttpUrl()). See #1356.useAgentChat()now treats a missing HTTP URL as "not ready yet":- The built-in
/get-messagesfetch is deferred until the URL is known, and applied exactly once when it resolves (empty chats only — existing messages are never overwritten). - Custom
getInitialMessagescallbacks continue to run and are passedurl: undefinedso they can load from other sources if they don't need the socket URL.GetInitialMessagesOptions.urlis nowstring | undefined; callers that previously typedurl: stringshould widen tourl?: string. - Initial messages are cached by agent identity (class + name) rather than by URL + identity, so the URL-arrival transition no longer invalidates the cache, re-invokes the loader, or re-triggers Suspense once the chat has already been populated.
- The underlying
useChatinstance keeps a stableidacross the URL-arrival transition, so in-flight stream resume and chat state are preserved.
No API or behavior changes for apps where the URL was already available synchronously on first render.
- The built-in
agents@0.11.4
Patch Changes
- #1222
3ebd966Thanks @Muhammad-Bin-Ali! - Add experimental WebMCP adapter (agents/experimental/webmcp) that bridges MCP server tools to Chrome's nativenavigator.modelContextAPI, enabling browser-native AI agents to discover and call tools registered on a Cloudflare McpAgent.
agents@0.11.3
Patch Changes
-
#1330
b4d3fcfThanks @threepointone! - FixsubAgent()cross-DO I/O errors on first use and drop the"experimental"compatibility flag requirement.subAgent()cross-DO I/O fixThree issues in the facet initialization path caused
"Cannot perform I/O on behalf of a different Durable Object"errors when spawning sub-agents in production:subAgent()constructed aRequestin the parent DO and passed it to the child viastub.fetch(). TheRequestcarried native I/O tied to the parent isolate, which the child rejected.- The facet flag was set after the first
onStart()ran, sobroadcastMcpServers()fired with_isFacet === falseon the initial boot. _broadcastProtocol(), the inheritedbroadcast(), and_workflow_broadcast()iterated the connection registry without an_isFacetguard, letting broadcasts reach into the parent DO's WebSocket registry from a child isolate.
Replaces the fetch-based handshake with a new
_cf_initAsFacet(name)RPC that runs entirely in the child isolate, sets_isFacetbefore init, and seeds partyserver's__ps_namekey directly. Adds_isFacetguards to_broadcastProtocol()and overridesbroadcast()to no-op on facets so downstream callers (chat-streaming paths, workflow broadcasts, userthis.broadcast(...)) are covered. Removes the previous internal_cf_markAsFacet()method —_cf_initAsFacet(name)is the correct entry point (it sets the flag before running the firstonStart(), which_cf_markAsFacetdid not)."experimental"compatibility flag no longer requiredctx.facets,ctx.exports, andenv.LOADER(Worker Loader) have graduated out of the"experimental"compatibility flag in workerd.agentsand@cloudflare/thinkno longer require it:subAgent()/abortSubAgent()/deleteSubAgent()— the@experimentalJSDoc tag and runtime error messages no longer reference the flag. The runtime guards onctx.facets/ctx.exportsstay in place and now nudge users toward updatingcompatibility_dateinstead.Think— the@experimentalJSDoc tag no longer references the flag.
No code change is required; remove
"experimental"from yourcompatibility_flagsinwrangler.jsoncif it was only there for these features. -
#1332
7cb8acfThanks @threepointone! - ExposecreatedAton fiber and chat recovery contexts so apps can suppress continuations for stale, interrupted turns.FiberRecoveryContext(fromagents) gainscreatedAt: number— epoch milliseconds whenrunFiberstarted, read from thecf_agents_runsrow that was already tracked internally.ChatRecoveryContext(from@cloudflare/ai-chatand@cloudflare/think) gains the samecreatedAtfield, threaded through from the underlying fiber.
With this, the stale-recovery guard pattern described in #1324 is a short override:
override async onChatRecovery(ctx: ChatRecoveryContext): Promise<ChatRecoveryOptions> { if (Date.now() - ctx.createdAt > 2 * 60 * 1000) return { continue: false }; return {}; }
No behavior change for existing callers. See
docs/chat-agents.md(new "Guarding against stale recoveries" section) for the full recipe, including a loop-protection pattern usingonChatResponse.
@cloudflare/worker-bundler@0.1.2
Patch Changes
-
#1334
77c8c9cThanks @threepointone! -createWorkerandcreateAppnow accept a handful of extra esbuild knobs that previously required forking or patching the package:jsx("transform" | "preserve" | "automatic")jsxImportSourcedefine(compile-time constant replacement)loader(per-extension loader overrides — e.g.{ ".svg": "text", ".wasm": "binary" }; built-in handling for.ts/.tsx/.js/.jsx/.json/.cssis preserved unless overridden, and longer extensions match first so".d.ts"wins over".ts"). The accepted values are deliberately narrowed to the portableBundlerLoaderset (js/jsx/ts/tsx/json/css/text/binary/base64/dataurl) — esbuild-specific loaders likefile/copy/empty/defaultare intentionally excluded.file/copywould silently break in this bundler today (they emit secondary output files that get discarded), and anything outside the portable set should go through the plugin escape hatch instead.conditions(package export conditions, e.g.["workerd", "worker", "browser"])
The first five are re-typed locally (
JsxMode,BundlerLoader) so the published.d.tsdoes not import fromesbuild-wasm— a future bundler swap is a refactor, not a breaking type change.For advanced consumers (RSC-style transforms, custom asset pipelines, codegen) there is also an explicit escape hatch:
__dangerouslyUseEsBuildPluginsDoNotUseOrYouWillBeFired?: unknown[]
The deliberately unwieldy name is the API contract: this option is not covered by semver, can change shape or be removed in any release, and ties the caller to esbuild's plugin shape — if this package switches bundlers, plugins authored against it will break. It is typed as
unknown[]at the public boundary (castPlugin[]fromesbuild-wasmwhen passing in) so the published types don't acquire a hard dependency on esbuild. User plugins run before the internal virtual-filesystem plugin, so theironResolve/onLoadclaims fire first.In
createApp, all of these options apply to both the server and client bundles.The internal
bundleWithEsbuildsignature was refactored from a long positional argument list to a single options object so future bundler knobs can be added without churning every call site. This is an internal change; no public API moved.Inspired by #1321 — thanks @bndkt for the draft and the RSC-on-Workers proof-of-concept that motivated it.
-
#1335
e59388dThanks @threepointone! - Fix: don't crash withCannot find package 'gojs'when imported from Node.Previously,
bundler.tsdid a top-level staticimport esbuildWasm from "./esbuild.wasm". In the Workers runtime that resolves to aWebAssembly.Modulenatively, but in Node 22+ (e.g. Vitest on GitHub Actions CI) Node's experimental ESM-WASM loader actually parses the file and tries to resolveesbuild-wasm's Go-runtime import namespacegojsas an npm package. That surfaced as the deeply confusing error reported in #1306:Cannot find package 'gojs' imported from .../@cloudflare/worker-bundler/dist/esbuild.wasmTwo changes:
- The
./esbuild.wasmimport is now lazy — it lives insideinitializeEsbuild()as a dynamicimport("./esbuild.wasm")call instead of a module-level static import. The package is now safely importable from any JavaScript runtime. - Before evaluating that dynamic import, the bundler checks
navigator.userAgent === "Cloudflare-Workers". If it's not running inside workerd, it throws an actionable error pointing the caller at@cloudflare/vitest-pool-workersinstead of letting Node surface the crypticgojsresolution failure.
A side benefit:
createWorker({ bundle: false })(transform-only mode, which never invokes esbuild) now also works in Node, because the WASM is never loaded on that code path.The README now also calls out the Workers-only requirement near the top.
While in there, sharpened a handful of unhelpful error messages to include actionable context:
- "Entry point/Server entry point/Client entry point ... not found" now lists the user-provided files in the bundle (skipping
node_modules/) so it's obvious whether the path is mistyped vs. missing entirely. - "Could not determine entry point" now spells out the full priority list it tried (
entryPointoption → wranglermain→package.json→ defaults). - npm registry errors include the package name, version, registry URL, and HTTP status text — e.g.
Registry returned 404 Not Found for "hno" at https://registry.npmjs.org/hno (package not found — check the name in package.json or set theregistryoption if it lives on a private registry). - The npm fetch-timeout error names the URL and notes the registry was slow/unreachable from the Worker.
- "Invalid package.json" includes both the path and the underlying parse error.
- "No output generated from esbuild" now names the entry point and explains the two real-world causes (a custom plugin claiming the entry without returning contents, or the entry resolving to an externalised module).
- The
@cloudflare/think@0.3.0
Minor Changes
-
#1340
3cbe776Thanks @threepointone! - Align Think lifecycle hooks with the AI SDK and fix latent bugs around tool-call hooks and extension dispatch.Lifecycle hook context types are now derived from the AI SDK (resolves #1339).
StepContext,ChunkContext,ToolCallContext, andToolCallResultContextare derived fromStepResult,TextStreamPart, andTypedToolCallso users get full typed access toreasoning,sources,files,providerMetadata(where Anthropic cache tokens live),request/response, etc., instead ofunknown. The relevant AI SDK types are re-exported from@cloudflare/think.beforeToolCall/afterToolCallnow fire with correct timing.beforeToolCallruns before the tool'sexecute(Think wraps every tool'sexecute), andafterToolCallruns after withdurationMsand a discriminatedsuccess/output/erroroutcome (backed byexperimental_onToolCallFinish).ToolCallDecisionis now functional. Returning{ action: "block", reason },{ action: "substitute", output }, or{ action: "allow", input }frombeforeToolCallactually intercepts execution.Extension hook dispatch.
ExtensionManifest.hooksclaimed support forbeforeToolCall/afterToolCall/onStepFinish/onChunkbut Think only ever dispatchedbeforeTurn. All five hooks now dispatch to subscribed extensions with JSON-safe snapshots. Extension hook handlers also receive(snapshot, host)(symmetric with toolexecute); previously only tool executes got the host bridge.Breaking renames (per AI SDK conventions):
ToolCallContext.args→input,ToolCallResultContext.args→input,ToolCallResultContext.result→output.afterToolCallis now a discriminated union — readoutputonly whenctx.success === true, anderrorwhenctx.success === false. Equivalent renames onToolCallDecision.See docs/think/lifecycle-hooks.md for the full hook reference.
Patch Changes
- #1340
3cbe776Thanks @threepointone! - Fix_wrapToolsWithDecisiontoawait originalExecute(...)before checking forSymbol.asyncIterator. The previous code missedPromise<AsyncIterable>returns from plain async functions (async function execute(...) { return makeIter(); }) —Symbol.asyncIterator in promiseis always false, the collapse logic was skipped, and the AI SDK ended up treating the iterator instance itself as the final output value (which the wrapper's own comment warned about). Both sync-returned-iterable and async-returned-iterable cases are now covered, with regression tests for each.
@cloudflare/think@0.2.5
Patch Changes
-
#1330
b4d3fcfThanks @threepointone! - FixsubAgent()cross-DO I/O errors on first use and drop the"experimental"compatibility flag requirement.subAgent()cross-DO I/O fixThree issues in the facet initialization path caused
"Cannot perform I/O on behalf of a different Durable Object"errors when spawning sub-agents in production:subAgent()constructed aRequestin the parent DO and passed it to the child viastub.fetch(). TheRequestcarried native I/O tied to the parent isolate, which the child rejected.- The facet flag was set after the first
onStart()ran, sobroadcastMcpServers()fired with_isFacet === falseon the initial boot. _broadcastProtocol(), the inheritedbroadcast(), and_workflow_broadcast()iterated the connection registry without an_isFacetguard, letting broadcasts reach into the parent DO's WebSocket registry from a child isolate.
Replaces the fetch-based handshake with a new
_cf_initAsFacet(name)RPC that runs entirely in the child isolate, sets_isFacetbefore init, and seeds partyserver's__ps_namekey directly. Adds_isFacetguards to_broadcastProtocol()and overridesbroadcast()to no-op on facets so downstream callers (chat-streaming paths, workflow broadcasts, userthis.broadcast(...)) are covered. Removes the previous internal_cf_markAsFacet()method —_cf_initAsFacet(name)is the correct entry point (it sets the flag before running the firstonStart(), which_cf_markAsFacetdid not)."experimental"compatibility flag no longer requiredctx.facets,ctx.exports, andenv.LOADER(Worker Loader) have graduated out of the"experimental"compatibility flag in workerd.agentsand@cloudflare/thinkno longer require it:subAgent()/abortSubAgent()/deleteSubAgent()— the@experimentalJSDoc tag and runtime error messages no longer reference the flag. The runtime guards onctx.facets/ctx.exportsstay in place and now nudge users toward updatingcompatibility_dateinstead.Think— the@experimentalJSDoc tag no longer references the flag.
No code change is required; remove
"experimental"from yourcompatibility_flagsinwrangler.jsoncif it was only there for these features. -
#1332
7cb8acfThanks @threepointone! - ExposecreatedAton fiber and chat recovery contexts so apps can suppress continuations for stale, interrupted turns.FiberRecoveryContext(fromagents) gainscreatedAt: number— epoch milliseconds whenrunFiberstarted, read from thecf_agents_runsrow that was already tracked internally.ChatRecoveryContext(from@cloudflare/ai-chatand@cloudflare/think) gains the samecreatedAtfield, threaded through from the underlying fiber.
With this, the stale-recovery guard pattern described in #1324 is a short override:
override async onChatRecovery(ctx: ChatRecoveryContext): Promise<ChatRecoveryOptions> { if (Date.now() - ctx.createdAt > 2 * 60 * 1000) return { continue: false }; return {}; }
No behavior change for existing callers. See
docs/chat-agents.md(new "Guarding against stale recoveries" section) for the full recipe, including a loop-protection pattern usingonChatResponse.
@cloudflare/shell@0.3.3
Patch Changes
-
#1333
dce4d17Thanks @threepointone! -Workspaceis now idempotent on duplicate construction for the same{sql, namespace}when the options that affect durable storage (r2,r2Prefix,inlineThreshold) agree. Previously, any second construction threwWorkspace namespace "<ns>" is already registered on this agent, which wedged legitimate cases — most commonly Vite HMR re-evaluating a Durable Object's module against a still-livectx.storage.sql, and helpers that accept asqland construct a short-livedWorkspacealongside an existing class-field one.The guard is preserved where it actually catches a bug: if a second construction passes a different
r2,r2Prefix, orinlineThreshold, the constructor throws with a message naming the disagreeing field and both values — because diverging storage options silently route large files to different R2 keys or classify them at different sizes, so reads through one instance would fail to find data written via the other.onChangeis intentionally not part of the consistency check — eachWorkspaceinstance calls its own listener for its own writes, which is the existing per-instance semantic.