ai-chat: align with Think + multi-ai-chat example on the sub-agent routing primitive#1353
Merged
threepointone merged 12 commits intomainfrom Apr 22, 2026
Merged
ai-chat: align with Think + multi-ai-chat example on the sub-agent routing primitive#1353threepointone merged 12 commits intomainfrom
threepointone merged 12 commits intomainfrom
Conversation
🦋 Changeset detectedLatest commit: 6662d8f The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
agents
@cloudflare/ai-chat
@cloudflare/codemode
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
3 tasks
threepointone
added a commit
that referenced
this pull request
Apr 21, 2026
Spike extension confirms the cross-DO stub passthrough question the
RFC left open, and lands the answer in the design doc.
## Finding
- Returning a facet stub from a parent's RPC method fails with
DataCloneError ("Could not serialize object of type DurableObject").
Same for a top-level DO stub — this is a general limit, not
facet-specific.
- An `RpcTarget` wrapper that holds the facet stub and proxies an
`invoke(method, args)` surface *does* cross the boundary, but its
lifetime is scoped to the RPC call that returned it. Reusing the
reference across separate calls breaks with "internal error".
- The viable path is a stateless per-call bridge: the parent
exposes one RPC method (`invokeSubAgent(childName, method, args)`)
that resolves the facet via `this.subAgent(...)` (idempotent) and
dispatches the call fresh each time. The caller-side
`getSubAgentByName` wraps it in a JS Proxy so the public API
stays exactly as the RFC specified.
Cost: one extra RPC hop per call (caller → parent → facet).
Benefit: works across hibernation, no reference-lifetime gotchas.
Limitation: the returned Proxy supports RPC method calls only, not
`.fetch()`. External HTTP/WS routing goes through
`routeSubAgentRequest`.
Subtle gotcha pinned: the parent-side bridge must call
`handle[method](...args)` in one expression. Extracting via `const
fn = handle[method]; fn.apply(handle, args)` detaches the workerd
RpcProperty binding and throws.
## Tests
Spike file now has 9 tests (up from 5): the original WS/HTTP
passthrough confirmations, plus four covering the per-call bridge
(direct invoke, reading state mutated via WS, JS-Proxy ergonomics,
reuse across multiple independent calls).
## RFC updates
- Step 3 of implementation plan rewritten around the per-call
bridge and its gotcha. Previous direct-stub-return sketch + open
question replaced with the actual answer.
- D8 section for `getSubAgentByName` documents the .fetch()
limitation and the extra RPC hop.
- Decided section captures the finding.
- Tests section updated.
## Devin review feedback (PR #1355)
- "Broken link to rfc-ai-chat-maintenance.md" — the file lives on
PR #1353 (not yet on main). Replaced the dead relative link with
a PR reference that'll resolve.
- "New RFC not added to design/AGENTS.md Current contents table" —
added the row.
Made-with: Cursor
threepointone
added a commit
that referenced
this pull request
Apr 21, 2026
Updates both RFCs now that the routing implementation (phases 1–3) is on this branch. ## rfc-sub-agent-routing.md - Migration section rewritten: explicit about which downstream consumers migrate where. The Think multi-session RFC builds on top; `examples/multi-ai-chat` lives on #1353 and will rebase there; user-facing docs are a follow-up. ## rfc-think-multi-session.md - Adds a "Related" link to the routing RFC, marking it as landed. - Replaces the planned `parentAgent<T>()` helper with the real pattern using the now-shipped `this.parentPath` + a DO namespace lookup. Strictly more flexible (grandparents work too) and consistent with the example code. - `useChats()` is now explicitly a thin wrapper over the landed `useAgent({ sub: [...] })` primitive instead of a nested useAgent-in-useAgent dance. - The migration section removes the landed items (registry, parentPath, useAgent sub) and lists what still needs building (Chats class, RemoteContextProvider/SearchProvider, useChats, examples/chats, docs updates). - Summary section reflects the primitive is no longer hypothetical. No implementation changes in this commit — docs tracking reality. Made-with: Cursor
Mechanical alignments between `@cloudflare/ai-chat` and `@cloudflare/think`, paired with a stance RFC and a reference example for multi-session chat. ## Code changes - `AIChatAgent` gains a `Props` generic to match the Think change we just shipped: `AIChatAgent<Env, State, Props>` extending `Agent<Env, State, Props>`. `this.ctx.props` is typed now. - `ChatResponseResult`, `ChatRecoveryContext`, `ChatRecoveryOptions`, `SaveMessagesResult`, and `MessageConcurrency` move into `agents/chat/lifecycle.ts`. Both `@cloudflare/ai-chat` and `@cloudflare/think` import from `agents/chat` and re-export. No behavior change; one place to edit when the shapes evolve. - `AIChatAgent` drops the `UIMessage as ChatMessage` import alias and uses `UIMessage` everywhere. The `ChatMessage` type is no longer exported from `@cloudflare/ai-chat`. Internal `message-reconciler` also drops its local alias. - `AIChatAgent.messages` becomes a getter over a protected `_messages` backing field. Prevents `this.messages = [...]` reassignment from subclasses. The returned array type stays mutable for AI SDK compat (`convertToModelMessages(this.messages)` works unchanged); signatures on the `reconcileMessages` helpers and the `OutgoingMessage` wire type accept `readonly UIMessage[]` where they only read. ## Docs - `design/rfc-ai-chat-maintenance.md` captures the stance: `AIChatAgent` stays first-class and fully supported while `Think` stabilizes. New features land in `agents/chat` where both benefit. Deferred structural work (hoisting protocol handling, promoting `agents/chat` to a public toolkit, `onChatMessage` signature revision) is listed with rationale. ## Example - `examples/multi-ai-chat/` — a hand-rolled preview of the `Chats` pattern from `rfc-think-multi-session.md`, using `AIChatAgent` children. An `Inbox` parent DO owns the chat list + per-user shared memory; per-chat `AIChatAgent` DOs run in parallel. Client wires up via `useAgent` + `useAgentChat` directly, so when the `Chats` base class lands, the migration is ~10 lines. Made-with: Cursor
049564d to
7095cae
Compare
Rebuilds the example on top of the sub-agent routing primitive that landed in #1355. The original commit on this branch was written before that primitive existed and used two top-level DO bindings (`Inbox` + `Chat`) with direct namespace RPC between them. Now that the routing primitive is merged, the example can — and should — demonstrate it. ## Server (`src/server.ts`) - `Chat` becomes a **facet** of `Inbox`. No top-level binding; no namespace lookup for the child. `Inbox.createChat` calls `this.subAgent(Chat, id)` to spawn the facet and register it in the parent's sub-agent registry. `deleteChat` calls `this.deleteSubAgent(Chat, id)`. - `Inbox.onBeforeSubAgent` implements a strict-registry gate using `hasSubAgent`. A chat becomes reachable only after `createChat` has spawned it; unknown ids get a 404 before any facet is woken. - `Chat` reaches its parent via `this.parentPath[0]` — the root-first ancestor chain the framework populates at facet-init time. No hardcoded user id inside the chat. - Worker entry collapses to a one-line `routeAgentRequest` call: `/agents/inbox/{user}/sub/chat/{chatId}` is handled natively. ## Client (`src/client.tsx`) - `ActiveChat` connects via `useAgent({ agent: "Inbox", name: DEMO_USER, sub: [{ agent: "Chat", name: chatId }] })` — the hook builds the nested `/sub/chat/{chatId}` URL; everything downstream (identity, state sync, `useAgentChat`) works unchanged. The sidebar connection stays as a plain `useAgent({ agent: "Inbox", ... })`. ## Config - `wrangler.jsonc` drops the `Chat` top-level binding but keeps `Chat` in `new_sqlite_classes` so the runtime can still construct it as a facet. - `env.d.ts` drops the `Chat: DurableObjectNamespace<...>` entry for the same reason. ## Docs - README rewritten to describe the actual mechanics (URLs, hook gate, parentPath) rather than a forward-looking "Chats pattern sketch". Adds a link to the now-landed sub-agent routing RFC. - Changeset updated to note the example exercises the routing primitive end-to-end. The `Chats` base class from `rfc-think-multi-session.md` will collapse `Inbox`'s chat bookkeeping (create / delete / list / `onBeforeSubAgent` gate) into framework defaults. When that lands, this example's `Inbox` becomes ~10 lines. Made-with: Cursor
Two regressions surfaced by running the multi-ai-chat example:
**1. `keepAlive()` threw inside a facet, breaking streaming chats.**
`AIChatAgent._reply` wraps the streaming turn in `keepAliveWhile(...)`
to guarantee the DO finishes committing the final message even if
the client disconnects mid-stream. That path crashed every turn
inside a Chat facet with:
Error: keepAlive() is not supported in sub-agents.
The original guard assumed "facets delegate lifecycle to the parent"
but that left a real hole: a facet's `_reply` can't just give up
keepalive bookkeeping because the parent doesn't know about it.
workerd doesn't support independent alarms on facets yet ("alarms
are not yet implemented for SQLite-backed Durable Objects" when you
try), so the fix can't be "add an alarm on the facet". Instead,
make `keepAlive()` a **soft no-op** in facets: return an inert
disposer, don't throw. Facets piggyback on the parent isolate —
active Promise chains, WebSockets, and the parent's own alarm all
keep the shared isolate alive; the defensive keepalive is redundant
in that context. Documented in the JSDoc with a pointer at
"call `keepAlive()` on the parent via RPC if you really need it".
**2. `sendIdentityOnConnect` mis-warned for facet instances.**
The warning fires when the instance name isn't visible in the
URL — but it checks the request URL the DO itself sees, which for
a facet has been rewritten by `_cf_forwardToFacet` to strip
`/sub/{class}/{name}`. The CLIENT always put the name in the URL
(that's literally how sub-agent routing works). Suppress the
warning for facets; the concern doesn't apply.
Tests:
- `keepAlive() works inside a sub-agent` (no throw, returns a
working disposer)
- `keepAliveWhile() runs to completion inside a sub-agent` — same
call shape as AIChatAgent._reply, pins the multi-ai-chat
regression
- The old "keepAlive throws in facets" assertion is flipped to
assert it succeeds.
Made-with: Cursor
**User-visible bug**: In `examples/multi-ai-chat`, the assistant's streaming reply didn't appear in the chat UI until the user refreshed the page. The sidebar "last message preview" updated in real time (it goes through `recordChatTurn` RPC to the parent Inbox), but the streaming chunks never reached the browser over the WebSocket. On refresh, `/get-messages` fetched the persisted turn from the facet's SQLite and it showed up — so data was being written; only live broadcast was silent. **Root cause**: two guards in `Agent` — an early-return in `_broadcastProtocol` and an override on `broadcast` itself — that no-op'd whenever `_isFacet` was true. The comments explained the concern: > Facets share the parent DO's WebSocket registry: getConnections() > returns parent-owned sockets, so iterating from a facet throws > "Cannot perform I/O on behalf of a different Durable Object". > Sub-agents are RPC-only and have no WS clients of their own. That was accurate for the pre-routing world where facets existed only as RPC targets reachable by the parent. Sub-agent routing (#1355) changed the model: clients now connect directly to facets via `/agents/{parent}/{name}/sub/{class}/{name}`, and those WebSockets are upgraded on — and owned by — the facet's isolate. `getConnections()` inside the facet returns the facet's own sockets. The "cross-DO I/O" concern no longer applies. The consequence was that every `this.broadcast(...)` call on a facet silently did nothing. That includes: - `AIChatAgent._broadcastChatMessage` — streaming chunks to the client during a chat turn. **This is the one that broke the demo.** - `setState()` → `_broadcastProtocol` → `CF_AGENT_STATE` — state sync to connected clients from a facet. - `broadcastMcpServers` — MCP server updates. - Any user-defined broadcast from subclass code. **Fix**: remove both guards. `this.broadcast(...)` and `this._broadcastProtocol(...)` now iterate the facet's own connections — same behavior as a top-level DO. Regression test (spike suite): a facet is connected to directly, then invokes `this.broadcast(...)` from `onMessage`. The client receives the broadcast. Before this fix the broadcast was silently dropped; now it round-trips. Other `_isFacet` guards are unchanged: - `schedule()` / `cancelSchedule()` / `keepAlive()` still special-case facets — workerd doesn't support alarms on SQLite-backed facets today. The previous commit documents `keepAlive`'s soft-no-op semantics. - `destroy()`'s `deleteAlarm` skip for facets stays (facets never set alarms, so there's nothing to clear). Fixes the "chat UI doesn't update until refresh" symptom in `examples/multi-ai-chat`. Made-with: Cursor
…ory tools Three small tools on the `Chat` agent to make the demo actually agentic: - `rememberFact(fact)` — persists a fact to the parent Inbox's shared memory (`inbox.setSharedMemory`). Every sibling chat picks up the fact on the next turn. Demonstrates cross-DO RPC from inside a tool `execute` that runs in a facet. - `recallMemory()` — reads the current shared memory. - `getCurrentTime()` — returns the server's ISO time. Included mostly to give the model a tool to pick when the user just wants small talk about the clock. The model now runs in a multi-step agentic loop (`stopWhen: stepCountIs(5)`) so it can call a tool, observe the output, and respond in the same turn. Client rendering overhaul: - Drop the "join all text parts into one string" renderer. - Render `UIMessage.parts` in order: text → bubble, `reasoning` → dimmed "Thinking" block, tool parts → panel with state badge (Running/Done/Error), input JSON, output JSON, and errorText. - Streaming cursor only appears on the trailing text part of the last assistant message. - Ignore `step-start`, `source-*`, `file` — the `examples/ai-chat` has a fuller treatment if needed. README points people at things to try: _"Remember I prefer TypeScript"_ exercises `rememberFact`, and _"What time is it?"_ exercises `getCurrentTime`. Saving memory via the sidebar still works for the no-tool-call case. Made-with: Cursor
Five small follow-ups from a self-review pass on the PR. All tests
pass (1325/1325 in agents); all 75 projects typecheck.
**1. `parentAgent<T>(namespace)` on the Agent base class.**
Every facet-based app was about to hand-roll a `getParent()` helper
that reads `this.parentPath[0]` and opens a stub via `getAgentByName`.
Codify it on the base class — pass the parent's namespace binding,
get back a typed `DurableObjectStub<T>` with the right instance
name resolved for you:
class Chat extends AIChatAgent<Env> {
private getInbox() {
return this.parentAgent(this.env.Inbox);
}
}
Throws a clear error when called on a top-level (non-facet) agent.
Tests: `resolves the parent stub from within a facet`, `throws a
clear error when called on a non-facet`.
**2. `examples/multi-ai-chat`: `listSubAgents(Chat)` as the source of truth.**
Previously the example maintained a parallel `inbox_chats` table
alongside `cf_agents_sub_agents` — both tracked "this chat exists",
and a crash between the two writes could leave them out of sync.
Now: the sub-agent registry is authoritative for existence, and a
thin `chat_meta` table holds app-owned decoration (title, preview,
updated_at). `_refreshState` joins `listSubAgents(Chat)` against
`chat_meta` to build the sidebar. A chat with a missing meta row
just gets a default title.
**3. Drop the redundant `className !== "Chat"` check in `onBeforeSubAgent`.**
`Agent.fetch` filters URLs via `knownClasses: Object.keys(ctx.exports)`
before the hook runs, so by the time `onBeforeSubAgent` fires the
class is guaranteed to be in exports. The subsequent `hasSubAgent`
check acts as the real gate.
**4. `Chat.getInbox()` now delegates to `this.parentAgent(...)`.**
Two hardcoded ancestor-shape assertions collapse into the framework
helper.
**5. Client `AnyToolPart` type cleanup.**
Drop the hand-rolled intersection type. `ToolPart` now takes
`Parameters<typeof getToolName>[0]` — the same narrowed union
`isToolUIPart` returns — and reads optional fields via `"x" in part`
checks instead of re-widening. Type-safe with no casts.
**6. Trim `keepAlive()` docstring.**
The previous text pointed users at `getAgentByName(parent).keepAlive()`
as an escape hatch. In practice nobody needs it — the soft no-op is
sufficient because facets share the parent's isolate and the active
Promise chain plus open WebSockets already keep the machine alive
for the duration of real work.
Made-with: Cursor
The `parentAgent(namespace)` signature from the previous commit had
a silent-corruption footgun: passing the wrong binding resolved a
stub for a different DO against the recorded parent name. If the
target class happened to share method names with the recorded
parent, calls would succeed silently against the wrong data.
Change the API to take a class reference (symmetric with
`subAgent(Cls, name)` on the parent side), plus two runtime
guards:
1. `cls.name === parentPath[0].className` — catches the wrong-class
mistake directly. Error names both the passed and the recorded
class so the diagnostic is actionable.
2. `env[cls.name]` exists — catches the "binding name ≠ class name"
case with a suggestion to use `getAgentByName(env.X, this.parentPath[0].name)`
directly.
Usage collapses from
await this.parentAgent(this.env.Inbox as DurableObjectNamespace<Inbox>)
to
await this.parentAgent(Inbox)
Symmetric with `this.subAgent(Chat, id)`.
JSDoc now also documents how to reach grandparents (iterate
`this.parentPath`; there's no framework helper for further
ancestors — the one-hop case is 95% of usage).
Example `multi-ai-chat`:
- `Chat.getInbox()` uses the new form: `this.parentAgent(Inbox)`.
- `Inbox.onBeforeSubAgent` now returns a class-agnostic
`"${className} "${name}" not found"` body (previously said
"Chat not found" for anything, stale after we dropped the
className-equality guard).
Tests:
- Existing `resolves the parent stub from within a facet` test now
exercises the class-ref form (casts dropped).
- New `throws when the passed class doesn't match the recorded
parent class` test verifies the class-mismatch guard. Asserts
both class names appear in the error body.
Made-with: Cursor
Implements the review decisions directly:
1. **`messages` stays a public field.**
Revert the getter + `_messages` backing field experiment in
`AIChatAgent`. The compatibility cost was real, the benefit was
thin, and existing subclasses may legitimately assign
`this.messages = [...]` or mutate it directly. Internals now write
`this.messages` again.
2. **`ChatMessage` stays exported.**
Internally the codebase still standardizes on `UIMessage`, but the
package now keeps `export type ChatMessage = UIMessage` so existing
user imports from `@cloudflare/ai-chat` do not break.
3. **Docs / README / changeset sweep.**
- `packages/ai-chat/README.md`
- API header updated to `AIChatAgent<Env, State, Props>`
- `messages` described as public + mutable for compatibility
- exports table includes `ChatMessage`
- `docs/chat-agents.md`
- `ChatRecoveryContext.messages` → `UIMessage[]`
- stale `this.messages = []` example → `await this.saveMessages([])`
- top-level `README.md`
- adds Sub-agents feature row
- includes `examples/multi-ai-chat` in the examples tour
- `packages/agents/README.md`
- adds a new Sub-agents section (`subAgent`, `onBeforeSubAgent`,
`useAgent({ sub })`, `parentAgent`)
- `packages/agents/AGENTS.md`
- refreshes the source layout (`sub-routing.ts`, `chat/`)
- adds `agents/chat` export, but explicitly frames it as a
sibling-package support layer rather than a broad user-facing
surface
- updates the stale `src/index.ts` line count and test-suite list
- `design/AGENTS.md`
- adds missing entries for `rfc-think-multi-session.md` and the
AIChatAgent stance RFC
- `.changeset/ai-chat-cleanups.md`
- reflects the actual compatibility decisions (`ChatMessage`
kept, `messages` stays mutable, `parentAgent(Inbox)` in the
example)
4. **Rewrite the AIChatAgent RFC around the real stance.**
`design/rfc-ai-chat-maintenance.md` is now:
- retitled to remove "maintenance"
- marked `Status: accepted`
- explicit that `AIChatAgent` is first-class, production-ready,
and continuing to get features
- corrected to say `messages` stays mutable and `ChatMessage`
stays exported
- reframed `agents/chat` as primarily a sibling-package shared
toolkit today (published, versioned, but not yet over-marketed)
5. **Update RFCs for shipped reality.**
- `rfc-think-multi-session.md` now reflects the shipped
`parentAgent(Cls)` helper instead of the old generic / manual
`parentPath` lookup text.
- `rfc-sub-agent-routing.md` now reflects `className`,
`parentAgent(Cls)`, current `listSubAgents` return shape, and the
post-launch facet semantics (facet broadcasts, keepAlive no-op).
Checks:
- `npm run check` — all 75 projects typecheck successfully
- `packages/ai-chat` workers tests — 414/414 passing
- `packages/agents` workers tests — 1005/1005 passing (7 skipped)
Note: full workspace browser projects still require Playwright browsers
installed locally; they were not runnable in this environment.
Made-with: Cursor
Normalizes the user-facing wording around AIChatAgent: - is the public message type name in docs / README / changeset / RFCs - is described simply as the public field users already know, without compatibility framing - removes the lingering / language from user-facing AIChat docs This matches the actual public stance: we never shipped a breaking change to , and we don't need to narrate the public API as an apology for a change that never landed. Also updates the chat API design doc so the analysis uses the same public terminology () instead of oscillating between and . Made-with: Cursor
Four independent review fixes:
1. parentAgent root-vs-direct-parent bug (real, silent-corruption
footgun). parentPath is root-first, so the direct parent is the
LAST entry, not the first. The previous implementation did
`const [parent] = this._parentPath` which destructures the first
element — fine for one-level chains (Root -> Chat), but for any
deeper chain (Root -> Outer -> Inner) `parentPath[0]` is the root
and not the spawning parent. `parentAgent(Outer)` from Inner
would then either throw a confusingly wrong class-match error,
or — if the caller passed `Root` to silence the error — quietly
resolve a stub to the wrong DO.
Fix: use `this._parentPath[this._parentPath.length - 1]`. Update
the JSDoc and the diagnostic error messages to reference
`parentPath.at(-1)`. Regression test added: a doubly-nested
Inner facet calling `parentAgent(TestSubAgentParent)` must throw
with the real direct parent `OuterSubAgent` named in the error.
2. `_cf_initAsFacet` JSDoc claimed setting `_isFacet` early was
needed so broadcasts would be suppressed during the first
`onStart()`. That guard was removed in `e5827d54` ("let facets
broadcast to their own WebSocket clients"). The note has been
rewritten to reflect the actual remaining reason (schedule
guards still branch on `_isFacet`, not broadcasts).
3. Example violated the "no `dark:` Tailwind variants" rule in
`examples/AGENTS.md`. Replaced `bg-red-50 dark:bg-red-950/20` /
`text-red-600 dark:text-red-400` with the Kumo semantic tokens
(`bg-kumo-danger-tint`, `text-kumo-danger`).
4. Example was missing the required `public/favicon.ico`. Copied
from `examples/assistant/public/favicon.ico`.
Also updated the server header comment in `examples/multi-ai-chat/src/server.ts`
and the `rfc-sub-agent-routing.md` note about "the last entry of
parentPath" so the public docs match the implementation.
Made-with: Cursor
This fills the biggest documentation gap around sub-agents / facets:
there was no single user-facing page that explained the shipped
primitive end-to-end. Users had to piece it together from the routing
RFC, the Think-specific `chat()` docs, the long-running-agents guide,
and the multi-ai-chat example.
## New: `docs/sub-agents.md`
A dedicated user-facing reference page covering the primitive as it
works today:
- what a sub-agent / facet is
- when to use it vs a top-level DO
- `subAgent`, `deleteSubAgent`, `abortSubAgent`
- `onBeforeSubAgent`
- `hasSubAgent`, `listSubAgents`
- `parentPath`, `selfPath`, `parentAgent(Cls)`
- `useAgent({ sub: [...] })`
- `routeSubAgentRequest`, `getSubAgentByName`
- lifecycle / routing flow
- current limitations (no independent alarms on facets **yet**)
- link to the multi-ai-chat example
## Fix: `docs/long-running-agents.md`
The existing "Delegating to sub-agents" section said:
> Sub-agents are independent Durable Objects. They have their own
> state, their own schedules, and their own lifecycle.
That is not true today. Facets have their own state and lifecycle,
but *not* their own alarms. `schedule()` / `scheduleEvery()` are
unsupported on facets at the moment. The text now says so explicitly,
notes that support is coming soon, and points readers at the new
sub-agents page for the full routing / client / parent-lookup story.
## Navigation
- `docs/index.md` now links to `./sub-agents.md` under Core Concepts.
- `docs/think/sub-agents.md` now makes its scope explicit: it covers
Think's `chat()` RPC method and programmatic turns, while the
generic framework primitive lives in `../sub-agents.md`.
## Design docs
- Add `design/sub-agent-routing.md` as the living design doc for the
shipped primitive (the RFC remains the historical decision record).
- Register it in `design/AGENTS.md` and `design/README.md`.
- Fix one confusing example in `design/rfc-sub-agent-routing.md`
where the array order in the `parentPath` example contradicted the
comment (`root -> direct parent`).
Made-with: Cursor
Insert a <link rel="icon" href="/favicon.ico" /> tag into the head of examples/multi-ai-chat/index.html so the page displays the site favicon and improves UX.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Mechanical alignments between
@cloudflare/ai-chatand@cloudflare/think, a stance RFC, and a reference example that exercises the sub-agent routing primitive end-to-end.Builds on:
subAgent,onBeforeSubAgent,useAgent({ sub }),parentPath,hasSubAgent/listSubAgents)Code changes
Propsgeneric onAIChatAgent—AIChatAgent<Env, State, Props>extendingAgent<Env, State, Props>. Same change we shipped for Think.this.ctx.propstyped.ChatResponseResult,ChatRecoveryContext,ChatRecoveryOptions,SaveMessagesResult,MessageConcurrencynow live inagents/chat/lifecycle.ts. Both@cloudflare/ai-chatand@cloudflare/thinkimport and re-export fromagents/chat. No behavior change; one place to edit.UIMessageeverywhere — dropped theUIMessage as ChatMessageimport alias in@cloudflare/ai-chatand the internalmessage-reconciler. TheChatMessagetype is no longer exported from@cloudflare/ai-chat(breaking; users switch toUIMessagefrom"ai").messagesis a getter —get messages(): UIMessage[]over aprotected _messages: UIMessage[]. Preventsthis.messages = [...]reassignment from subclasses. Return type stays mutable for AI SDK compat (convertToModelMessages(this.messages)works unchanged). Reconciler helpers and theOutgoingMessagewire type acceptreadonly UIMessage[]where they only read.Docs
design/rfc-ai-chat-maintenance.md— explicit stance:AIChatAgentstays first-class and fully supported while Think stabilizes.agents/chatso both base classes benefit.agents/chat, promotingagents/chatto a first-class public toolkit, revising theonChatMessagesignature, resumable-stream consolidation, and the session-integration non-goal.Example
examples/multi-ai-chat/— multi-session chat built on the sub-agent routing primitive from sub-agent routing: RFC + implementation (phases 1–3) #1355:Inboxis a top-levelAgentper user. It owns the chat list (broadcast via state), per-user shared memory, and is the RPC surface for create/delete/rename.Chatis anAIChatAgentfacet of the inbox — no top-level DO binding.Inbox.createChatcallsthis.subAgent(Chat, id).Inbox.onBeforeSubAgentis a strict-registry gate: only chats that exist in the sidebar can be addressed (hasSubAgent+ 404 otherwise).Chatreaches its parent viathis.parentPath[0]— no hardcoded user id inside the child.useAgent({ agent: "Inbox", name: DEMO_USER, sub: [{ agent: "Chat", name: chatId }] }).routeAgentRequest(request, env)— the nested/agents/inbox/{user}/sub/chat/{chatId}shape is handled natively.The
Chatsbase class fromrfc-think-multi-session.mdwill land as sugar over this pattern (~10-lineInbox); this PR makes sure the primitives it wraps already work end-to-end.Test plan
npm run check— all 75 tsconfigs typecheck, sherif/exports/oxfmt/oxlint all clean@cloudflare/ai-chattests: 459/459 passing@cloudflare/thinktests: 249/249 passingagentstests: 1321/1321 passingexamples/multi-ai-chattypechecks with theChattop-level binding removedexamples/multi-ai-chatend-to-end in the browser — not blocking CIMade with Cursor