Releases: cloudflare/agents
@cloudflare/shell@0.3.5
Patch Changes
-
19a4c08Thanks @threepointone! - Bump dependencies:isomorphic-gitfrom^1.37.5to^1.37.6(runtime) and@cloudflare/vitest-pool-workersfrom^0.15.0to^0.15.1(devDependency, test-only — does not affect the published artifact).No API or runtime behavior change in
@cloudflare/shellitself.
@cloudflare/ai-chat@0.5.3
Patch Changes
-
ca510d4Thanks @threepointone! - Tighten theagentspeer dependency floor from>=0.8.7to>=0.11.7to reflect the current monorepo set we actually test against. Upper bound (<1.0.0) is unchanged.No runtime change in
@cloudflare/ai-chatitself. The visible effect for consumers: pairing the latest@cloudflare/ai-chatwith a staleagents(<0.11.7) now produces a peer warning where it previously did not. That's the intended signal —agentsversions older than 0.11.7 are no longer tested against this@cloudflare/ai-chat. -
#1411
2fa68beThanks @threepointone! - Addoptions.signaltoAIChatAgent.saveMessagesandcontinueLastTurnfor external cancellation of programmatic turns, plus protectedabortRequest(id)/abortAllRequests()methods (#1406).saveMessagesandcontinueLastTurnaccept a secondSaveMessagesOptionsargument:const result = await this.saveMessages(messages, { signal: controller.signal, }); if (result.status === "aborted") { // Inference loop terminated mid-stream; partial chunks persisted. }
The signal is linked to AIChatAgent's per-turn
AbortControllerand produces the same end state as achat-request-cancelWebSocket message: the inference loop's signal aborts, partial chunks persist, the result reportsstatus: "aborted", andonChatResponsefires with the same status. Pre-aborted signals short-circuit before any model work runs. Listeners are detached cleanly when the turn finishes, so the same long-lived signal can be passed to many turns without leaking.abortRequest(id, reason?)andabortAllRequests()are protected entry points for subclasses that want to cancel turns without tracking ids.SaveMessagesResult.statusnow includes"aborted"alongside"completed"and"skipped". Existing callers that only switch on"completed"are unaffected.Limitations.
AbortSignalcannot cross Durable Object RPC. Construct the controller inside the DO that callssaveMessages.- The signal lives in memory only. If the DO hibernates mid-turn and
chatRecoveryis enabled, the recovered turn runs without the original signal.
See
cloudflare/agents#1406for the motivating use case.
agents@0.11.6
Patch Changes
-
#1393
5aaf7c4Thanks @threepointone! - Migrate facet (sub-agent) bootstrap to the documented Cloudflare facet API: passid: parentNs.idFromName(name)toctx.facets.get()so the facet has its ownctx.id.name. Drops the__ps_namestorage write andsetName()bootstrap from_cf_initAsFacet.Why this matters. Facets spawned without an explicit
idinherit the parent DO'sctx.id, so on a facetctx.id.namewas the parent's name andthis.namesilently misreported as the parent's name. Anything that readthis.namefrom inside a sub-agent (includingselfPath,parentPath, and any user code) was getting the wrong value. With the explicitidpassed at facet creation time, the runtime gives the facet a realctx.id.name === nameand PartyServer's existing 0.5.xnamegetter resolvesthis.namecorrectly without any override mechanism, storage write, or cold-wake hydrate cost. Cold-wake recovery happens for free becauseidFromNameis deterministic and the factory re-runs on resume.This requires
partyserver≥ 0.5.3 (bumped in this release); 0.5.3 is byte-identical to 0.5.2 at runtime, only adds documentation and test coverage of the explicit-idfacet pattern.Other changes:
- New error path. If
subAgent()is called from a parent class that isn't bound as a Durable Object namespace, the framework now throws a descriptive error pointing atwrangler.jsonc. Ifthis.constructor.namelooks minified (e.g._a), the message includes a bundler-config hint about preserving class names. - Defensive runtime check.
_cf_initAsFacetnow assertsthis.name === nameso any future bug in the parent's id construction surfaces immediately instead of silently mis-identifying the facet. alarm()docstring clarified to reflect the new resolution path (this.namefromctx.id.name, not from a storage hydrate).- MCP test cleanup. Vestigial
setName("default")+ explicitonStart()call pairs inoauth2-mcp-client,wait-connections-e2e, andcreate-oauth-providertest files have been removed; they were originally needed for partyserver 0.4.x bootstrap but became actualctx.id.namemismatches under partyserver 0.5.x.
Backward-compatible for all public APIs:
subAgent(),parentAgent(),hasSubAgent(),listSubAgents(),deleteSubAgent(), andabortSubAgent()keep their signatures and semantics. The change is purely in the facet bootstrap internals; the user-facing effect is thatthis.nameinside a sub-agent now correctly reports the sub-agent's own name (was previously the parent's name when run against partyserver 0.5.x).See cloudflare/partykit#386 for the partyserver-side documentation companion.
- New error path. If
-
#1395
63cfae6Thanks @threepointone! - Share submit concurrency bookkeeping throughagents/chatand use it from both chat agents.This extracts the
latest/merge/drop/debounceadmission state machine into aSubmitConcurrencyControllerexported fromagents/chat.AIChatAgentsemantics (including merge persistence) are preserved.Thinknow picks up the same pending-enqueue protection, so an overlapping submit is still detected while an accepted request is between admission and turn queue registration.Additional fixes:
Thinknow captures the turn generation immediately after admission and threads it into_turnQueue.enqueue, so a clear that lands between admission and queue registration cannot run a stale turn.- Pending-enqueue tracking is now bound to a release function tied to the controller's reset epoch, so a release from a pre-reset submit can no longer erase a post-reset submit's marker and let a third submit slip through as non-overlapping.
- Debounce cancellation correctly resolves all in-flight waiters instead of overwriting a single timer slot.
-
#1396
fdf5a8aThanks @threepointone! - Fix Think persisting a duplicate orphan assistant row when a user submits during a streaming tool turn (#1381).When
useAgentChatposts an in-flight assistant snapshot it minted optimistically (client-generated ID,state: "input-available"), Session's INSERT-OR-IGNORE-by-ID would store it as a separate row alongside the eventual server-owned assistant for the sametoolCallId. The next turn'sconvertToModelMessagesthen produced a malformed Anthropic prompt and the provider rejected it.reconcileMessagesandresolveToolMergeIdnow live inagents/chatand Think runs them in_handleChatRequestbefore persistence. Staleinput-availablesnapshots pick up the server's tool output viamergeServerToolOutputs, and any incoming assistant whosetoolCallIdalready exists on a server row adopts the server's ID so persistence updates the existing row instead of inserting an orphan.@cloudflare/ai-chatkeeps its existing reconciler behavior; the only change is that it now importsreconcileMessages/resolveToolMergeIdfromagents/chatinstead of a local file.
@cloudflare/think@0.4.1
Patch Changes
-
#1395
63cfae6Thanks @threepointone! - Share submit concurrency bookkeeping throughagents/chatand use it from both chat agents.This extracts the
latest/merge/drop/debounceadmission state machine into aSubmitConcurrencyControllerexported fromagents/chat.AIChatAgentsemantics (including merge persistence) are preserved.Thinknow picks up the same pending-enqueue protection, so an overlapping submit is still detected while an accepted request is between admission and turn queue registration.Additional fixes:
Thinknow captures the turn generation immediately after admission and threads it into_turnQueue.enqueue, so a clear that lands between admission and queue registration cannot run a stale turn.- Pending-enqueue tracking is now bound to a release function tied to the controller's reset epoch, so a release from a pre-reset submit can no longer erase a post-reset submit's marker and let a third submit slip through as non-overlapping.
- Debounce cancellation correctly resolves all in-flight waiters instead of overwriting a single timer slot.
-
#1394
a0a0d17Thanks @threepointone! - think: addbeforeSteplifecycle hook andoutputpassthrough onTurnConfig.beforeStep(ctx)— new lifecycle hook called before each AI SDK step in the agentic loop, wired tostreamText({ prepareStep }). Receives aPrepareStepContext(the AI SDK'sPrepareStepFunctionparameter —steps,stepNumber,model,messages,experimental_context) and may return aStepConfig(PrepareStepResult) to overridemodel,toolChoice,activeTools,system,messages,experimental_context, orproviderOptionsfor the current step. UsebeforeTurnfor turn-wide assembly andbeforeStepwhen the decision depends on the step number or previous step results. Resolves #1363.TurnConfig.output— new optional field onTurnConfigforwarded tostreamText. Accepts the AI SDK's structured-output spec (e.g.Output.object({ schema }),Output.text()) so a single agent can keep tools enabled on intermediate turns and return schema-validated structured output on a designated turn — without losing tools at model construction. Combine withactiveTools: []for providers that strip tools whenresponseFormat: "json"is active (e.g.workers-ai-provider). Resolves #1383.- New re-exports from
@cloudflare/think:PrepareStepFunction,PrepareStepResult,PrepareStepContext,StepConfig.
beforeStepis available to subclasses; it is not dispatched to extensions (the AI SDKprepareStepboundary surfaces non-serializable inputs likeLanguageModelinstances). The AI SDK does not exposeoutputormaxStepsper step — set those at the turn level viaTurnConfig. All other extension hook subscriptions are unchanged. -
#1372
040da0fThanks @threepointone! - Remove Think's unused internalsession_idconfig scaffolding and move Think's private config into a dedicatedthink_configtable.Older builds wrote Think-owned config into Session's shared
assistant_config(session_id, key, value)table even though Think never actually had top-level multi-session support and_sessionId()always returned the empty string. Think now stores its private config rows inthink_config(key, value), which better matches the shipped model of one Think Durable Object per conversation and avoids overloading Session's shared metadata table.Existing Durable Objects are migrated automatically on startup: legacy Think-owned keys stored in
assistant_configwithsession_id = ''are copied intothink_configbefore config reads and writes continue. -
#1396
fdf5a8aThanks @threepointone! - Fix Think persisting a duplicate orphan assistant row when a user submits during a streaming tool turn (#1381).When
useAgentChatposts an in-flight assistant snapshot it minted optimistically (client-generated ID,state: "input-available"), Session's INSERT-OR-IGNORE-by-ID would store it as a separate row alongside the eventual server-owned assistant for the sametoolCallId. The next turn'sconvertToModelMessagesthen produced a malformed Anthropic prompt and the provider rejected it.reconcileMessagesandresolveToolMergeIdnow live inagents/chatand Think runs them in_handleChatRequestbefore persistence. Staleinput-availablesnapshots pick up the server's tool output viamergeServerToolOutputs, and any incoming assistant whosetoolCallIdalready exists on a server row adopts the server's ID so persistence updates the existing row instead of inserting an orphan.@cloudflare/ai-chatkeeps its existing reconciler behavior; the only change is that it now importsreconcileMessages/resolveToolMergeIdfromagents/chatinstead of a local file. -
#1374
a6e22c3Thanks @threepointone! - Fix stream resumption on page refresh: do not broadcastcf_agent_chat_messagesfrom Think'sonConnectwhile a resumable stream is in flight.Previously, Think unconditionally sent a
cf_agent_chat_messagesframe on every new WebSocket connection. When a client refreshed during an active chat turn, that broadcast arrived in the same connect sequence ascf_agent_stream_resumingand overwrote the in-progress assistant message the client was about to rebuild from the resumed stream. The assistant reply would stay hidden until the server finished the turn and re-broadcast the persisted history.Now Think only broadcasts
cf_agent_chat_messageson connect when there is no active resumable stream. During an active stream the resume flow is the authoritative source of state:STREAM_RESUMINGtriggers replay of buffered chunks, and the final state broadcast happens when the turn completes. This matches the behavior thatAIChatAgentalready had.Marked the internal
_resumableStreamfield asprotected(previouslyprivate) so framework subclasses and focused tests can coordinate around the resume lifecycle. -
#1384
a7059d4Thanks @threepointone! - IntroduceWorkspaceLike— type thethis.workspacefield as the minimum surface Think actually uses instead of the concreteWorkspaceclass.Think'sworkspaceis now typed asWorkspaceLike(Pick<Workspace, "readFile" | "writeFile" | "readDir" | "rm" | "glob" | "mkdir" | "stat">) rather thanWorkspace.createWorkspaceTools()likewise accepts anyWorkspaceLike. The default runtime value is unchanged — a fullWorkspacebacked by the DO's SQLite — so the vast majority of consumers need no changes.This unlocks patterns like a shared workspace across multiple agents: a child agent can override
workspacewith a proxy that forwards each call to a parent DO via RPC, and the rest of Think's workspace-aware code (the builtin tools, lifecycle hooks) keeps working without cast gymnastics. Seeexamples/assistantfor the cross-chat shared workspace built on this.Consumers who use
createWorkspaceStateBackend(workspace)from@cloudflare/shell(codemode'sstate.*API) still need a concreteWorkspace— that helper reaches for more of the filesystem surface thanWorkspaceLikecovers.
@cloudflare/shell@0.3.4
Patch Changes
-
#1384
a7059d4Thanks @threepointone! - IntroduceWorkspaceFsLike— the minimumWorkspacesurface required byWorkspaceFileSystemandcreateWorkspaceStateBackend.WorkspaceFileSystem's constructor andcreateWorkspaceStateBackend's parameter both now accept anyWorkspaceFsLike(aPick<Workspace, …>of the 16 filesystem methods the adapter reaches for) rather than a concreteWorkspace. Non-breaking —Workspacestill satisfiesWorkspaceFsLikeso every existing call site keeps working without changes.This unlocks wrapping a real
Workspacebehind your own layer — most commonly a cross-DO proxy that forwards each call to a parent agent's workspace over RPC — and still using it as the storage for codemode'sstate.*sandbox API viacreateWorkspaceStateBackend. Seeexamples/assistantfor the end-to-end pattern withSharedWorkspace.
@cloudflare/ai-chat@0.5.2
Patch Changes
-
#1374
a6e22c3Thanks @threepointone! - FixuseAgentChatrecreating the AI SDK Chat instance — and orphaning any in-flightresumeStream— wheneveragent.nametransitions in place.The
useAgent({ basePath })+static options = { sendIdentityOnConnect: true }pattern lets the server own the Durable Object instance name. The browser starts with a placeholder ("default"), thenuseAgentmutates the agent object's.nameto the server-assigned value when the identity frame arrives.useAgentChatpreviously includedagent.namein the stable chat id it passed touseChat({ id }), so the transition changed the id and the AI SDK recreated the underlying Chat instance. The useEffect that fireschatRef.current.resumeStream()is keyed on the ref object, not the Chat instance, so it does not re-fire on recreation — the resumed stream kept feeding chunks into the orphaned Chat's state while React subscribed to the new Chat's state, so the user saw an empty assistant reply after a mid-stream refresh until the server's finalCF_AGENT_CHAT_MESSAGESbroadcast landed.useAgentChatnow distinguishes an in-placeagent.namemutation from a genuine "consumer switched chats" event by checking the agent object's reference identity:- same
agentreference,namemutation → not a chat switch; keep the Chat instance stable. - new
agentreference → chat switch; recompute the stable chat id so the AI SDK recreates the Chat against the new conversation.
The stable id is also still upgraded once from the identity-only fallback to the URL-resolved key when the WebSocket handshake completes.
Consumers who want to switch chats without remounting should pass a different
agentobject (e.g. a newuseAgent({...})call with a differentname). To get a completely fresh Chat (e.g. when mounting a different chat tab), the conventional React pattern —key={chatId}on the parent or swapping the subtree — continues to work. - same
-
#1395
63cfae6Thanks @threepointone! - Share submit concurrency bookkeeping throughagents/chatand use it from both chat agents.This extracts the
latest/merge/drop/debounceadmission state machine into aSubmitConcurrencyControllerexported fromagents/chat.AIChatAgentsemantics (including merge persistence) are preserved.Thinknow picks up the same pending-enqueue protection, so an overlapping submit is still detected while an accepted request is between admission and turn queue registration.Additional fixes:
Thinknow captures the turn generation immediately after admission and threads it into_turnQueue.enqueue, so a clear that lands between admission and queue registration cannot run a stale turn.- Pending-enqueue tracking is now bound to a release function tied to the controller's reset epoch, so a release from a pre-reset submit can no longer erase a post-reset submit's marker and let a third submit slip through as non-overlapping.
- Debounce cancellation correctly resolves all in-flight waiters instead of overwriting a single timer slot.
-
#1396
fdf5a8aThanks @threepointone! - Fix Think persisting a duplicate orphan assistant row when a user submits during a streaming tool turn (#1381).When
useAgentChatposts an in-flight assistant snapshot it minted optimistically (client-generated ID,state: "input-available"), Session's INSERT-OR-IGNORE-by-ID would store it as a separate row alongside the eventual server-owned assistant for the sametoolCallId. The next turn'sconvertToModelMessagesthen produced a malformed Anthropic prompt and the provider rejected it.reconcileMessagesandresolveToolMergeIdnow live inagents/chatand Think runs them in_handleChatRequestbefore persistence. Staleinput-availablesnapshots pick up the server's tool output viamergeServerToolOutputs, and any incoming assistant whosetoolCallIdalready exists on a server row adopts the server's ID so persistence updates the existing row instead of inserting an orphan.@cloudflare/ai-chatkeeps its existing reconciler behavior; the only change is that it now importsreconcileMessages/resolveToolMergeIdfromagents/chatinstead of a local file.
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