Status: accepted
Related:
rfc-think-multi-session.md— multi-session pattern (chat children + directory agent), applies equally toAIChatAgentchildren.think-vs-aichat.md— comparison of the two chat base classes.
AIChatAgentis first-class, in production, and getting features.- This PR aligns
AIChatAgentwithThinkwhere the change is clearly ergonomic: add aPropsgeneric, share lifecycle/result types viaagents/chat, and keep the publicAIChatAgentsurface centered onChatMessageand the existingmessagesfield. - Shared chat infrastructure belongs in
agents/chatwhen both classes benefit. That subpath exists primarily as a sibling-package toolkit today; we can formalize it further later if outside consumers emerge. - Multi-session support for
AIChatAgentneeds no new primitive — the sub-agent routing work plus theChatspattern fromrfc-think-multi-session.mdalready support it.examples/multi-ai-chatis the proof.
The public stance is simple:
AIChatAgentis first-class.AIChatAgentis production-ready.AIChatAgentcontinues to receive features, fixes, and examples.
Think is not a replacement project in waiting; it is a different chat base class with a different opinionated surface. Both coexist:
AIChatAgent— unopinionated base for users who want full control over the inference pipeline (onChatMessage(onFinish, options) => Response), direct ownership of the model call, and a thinner abstraction over the underlying AI SDK.Think— opinionated base (overridegetModel()/getTools()/configureSession(), the framework drives the inference loop, Session-backed storage). This is a better fit for users who want higher-level batteries included: Session integration, compaction, context blocks, FTS5 search, etc.
When a capability is obviously shared, it should live in agents/chat (or another shared layer) so both classes benefit. When a capability is specific to one model of use, it can live on the class that owns that model.
Before:
export class AIChatAgent<
Env extends Cloudflare.Env = Cloudflare.Env,
State = unknown
> extends Agent<Env, State> { ... }After:
export class AIChatAgent<
Env extends Cloudflare.Env = Cloudflare.Env,
State = unknown,
Props extends Record<string, unknown> = Record<string, unknown>
> extends Agent<Env, State, Props> { ... }Closes the gap we fixed in Think. this.ctx.props now typed.
ChatResponseResult, ChatRecoveryContext, ChatRecoveryOptions, SaveMessagesResult, MessageConcurrency were duplicated in both @cloudflare/ai-chat and @cloudflare/think. A new packages/agents/src/chat/lifecycle.ts owns them; both packages import and re-export from agents/chat. Zero behavior change; one place to edit when we tweak a shape.
The public type name here is still ChatMessage. That is what users see in docs and in their imports from @cloudflare/ai-chat.
Internally, some implementation code now uses UIMessage, which is fine — but that's an internal detail, not a public-facing API story. The package keeps exporting:
export type ChatMessage = UIMessage;The field stays exactly what users already expect:
messages: ChatMessage[] = [];No getter/setter story, no _messages backing field, no change in how subclasses reason about the property.
No new API. The Chats base class proposed in rfc-think-multi-session.md already declares:
export abstract class Chats<
Env extends Cloudflare.Env = Cloudflare.Env,
ChildClass extends SubAgentClass<Agent<Env>> = SubAgentClass<Agent<Env>>,
...
> extends Agent<Env, State, Props> {
abstract getChildClass(): ChildClass;
...
}ChildClass extends SubAgentClass<Agent<Env>> — AIChatAgent subclasses satisfy this because AIChatAgent extends Agent. No special case.
We ship examples/multi-ai-chat in this PR as a concrete, hand-rolled preview of the pattern. The example does not use the proposed Chats class (it doesn't exist yet), but it does mirror the shape the class will formalize: a parent agent owns the session index and shared memory, per-chat AIChatAgent children are spawned via subAgent(), and useAgent({ sub: [...] }) connects directly to each child facet.
That way, when Chats lands, the migration is ~10 lines.
These are structural changes that need real adoption signal before we commit to them. All can land later without breaking anything shipped now.
| Follow-up | Why deferred |
|---|---|
Hoist common protocol handling into agents/chat |
_handleChatRequest, the onMessage WS dispatch, _notifyStreamResuming / _handleStreamResumeRequest / _handleStreamResumeAck, the pending-resume-connection tracking, _reply-style chunk broadcast — near-identical between AIChatAgent and Think. A ChatProtocolBase helper (mixin or composition) could own these. Big lift, but the payoff is real: one bugfix location, consistent behavior. Want real usage of both classes to stabilize first so we don't encode accidental divergence. |
Formalize agents/chat as a broader external toolkit |
Today it's a published subpath export used primarily by sibling packages (@cloudflare/ai-chat, @cloudflare/think). Keep it stable and versioned, but don't oversell it in user-facing docs yet. If third-party chat base classes emerge, formalize the surface, naming, and docs more aggressively. |
onChatMessage signature cleanup |
Current: onChatMessage(onFinish, options) => Response. Leaks an AI SDK internal (StreamTextOnFinishCallback) and requires constructing a Response. A cleaner shape would drop onFinish (use options.onFinish or a lifecycle hook instead) and return a stream/iterable instead of a Response. Breaking change; only worth doing if we're confident of the final shape. |
chatRecovery default |
Think defaults true; AIChatAgent defaults false. Cosmetic but inconsistent. Not breaking to unify; pick a direction during Think stabilization. |
sanitizeMessageForPersistence hook parity |
AIChatAgent exposes this override point; Think does the equivalent internally. Expose in Think too, or document the difference. Minor. |
StreamTextOnFinishCallback leaking |
The onFinish callback type is an AI-SDK internal that leaks into onChatMessage's signature. Drop when we revise onChatMessage. |
| Resumable streams: sync/async consistency | Both classes have ResumableStream now (shared in agents/chat). Small API differences in how resume is triggered from the client; consolidate. |
Session integration for AIChatAgent |
Explicitly not doing this. Users who want Session-backed storage / compaction / context blocks / FTS5 use Think. AIChatAgent stays on its flat cf_ai_chat_agent_messages table. |
Direct deprecation of AIChatAgent |
Not in scope. We only revisit when Think is stable and has been used in anger. |
ChatMessage type name |
Keep ChatMessage as the public type name. Internal implementation details can use UIMessage, but that is not part of the user-facing story. |
- Keep the stance clear. No "deprecated" banners, no migration warnings, no hedged public language. Users who built on
AIChatAgentare fully supported, and new users should feel confident choosing it when its model of control is what they want. - No flag days. We don't plan to force users off
AIChatAgenton a timeline. If we ever do deprecate, that gets its own RFC. examples/multi-ai-chatships as a preview of theChatspattern. The RFC forChatsitself is still open; this is a concrete shape to point at while that one lands.ChatMessagestays exported andmessagesstays as-is. The public surface should read naturally, not as a compatibility footnote.
- Should
messagesbereadonlyor stay mutable? Decided: stay as-is. Users already understand the field; there is no need to add ceremony around it. - Should lifecycle types live in
agents/chat(current choice) or in a newagents/chat/lifecyclesubpath? Sub-subpath feels like overengineering for ~5 types. Rolled into theagents/chatbarrel. - Should
OutgoingMessageintypes.tsbe promoted intoagents/chattoo? Probably, but it's the WS wire protocol; a separate cleanup. Left as-is.
- Rewriting
AIChatAgentto be a thin wrapper overThink(see follow-up table). - Adding Session integration to
AIChatAgent. - Formalizing a chat-base-class factory (
createChatAgent(options)that produces subclasses). - Adding a client hook specific to
AIChatAgentmulti-session —useChats()(from therfc-think-multi-session.md) is agent-class-agnostic and works forAIChatAgentchildren.