Skip to content

ai-chat: align with Think + multi-ai-chat example on the sub-agent routing primitive#1353

Merged
threepointone merged 12 commits intomainfrom
ai-chat-cleanups
Apr 22, 2026
Merged

ai-chat: align with Think + multi-ai-chat example on the sub-agent routing primitive#1353
threepointone merged 12 commits intomainfrom
ai-chat-cleanups

Conversation

@threepointone
Copy link
Copy Markdown
Contributor

@threepointone threepointone commented Apr 21, 2026

Mechanical alignments between @cloudflare/ai-chat and @cloudflare/think, a stance RFC, and a reference example that exercises the sub-agent routing primitive end-to-end.

Builds on:

Code changes

  • Props generic on AIChatAgentAIChatAgent<Env, State, Props> extending Agent<Env, State, Props>. Same change we shipped for Think. this.ctx.props typed.
  • Shared lifecycle typesChatResponseResult, ChatRecoveryContext, ChatRecoveryOptions, SaveMessagesResult, MessageConcurrency now live in agents/chat/lifecycle.ts. Both @cloudflare/ai-chat and @cloudflare/think import and re-export from agents/chat. No behavior change; one place to edit.
  • UIMessage everywhere — dropped the UIMessage as ChatMessage import alias in @cloudflare/ai-chat and the internal message-reconciler. The ChatMessage type is no longer exported from @cloudflare/ai-chat (breaking; users switch to UIMessage from "ai").
  • messages is a getterget messages(): UIMessage[] over a protected _messages: UIMessage[]. Prevents this.messages = [...] reassignment from subclasses. Return type stays mutable for AI SDK compat (convertToModelMessages(this.messages) works unchanged). Reconciler helpers and the OutgoingMessage wire type accept readonly UIMessage[] where they only read.

Docs

  • design/rfc-ai-chat-maintenance.md — explicit stance:
    • AIChatAgent stays first-class and fully supported while Think stabilizes.
    • New features land in agents/chat so both base classes benefit.
    • Deferred structural work is enumerated with rationale: hoisting protocol handling into agents/chat, promoting agents/chat to a first-class public toolkit, revising the onChatMessage signature, 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:
    • Inbox is a top-level Agent per user. It owns the chat list (broadcast via state), per-user shared memory, and is the RPC surface for create/delete/rename.
    • Chat is an AIChatAgent facet of the inbox — no top-level DO binding. Inbox.createChat calls this.subAgent(Chat, id).
    • Inbox.onBeforeSubAgent is a strict-registry gate: only chats that exist in the sidebar can be addressed (hasSubAgent + 404 otherwise).
    • Chat reaches its parent via this.parentPath[0] — no hardcoded user id inside the child.
    • The client connects to the active chat via useAgent({ agent: "Inbox", name: DEMO_USER, sub: [{ agent: "Chat", name: chatId }] }).
    • Worker entry is a single routeAgentRequest(request, env) — the nested /agents/inbox/{user}/sub/chat/{chatId} shape is handled natively.

The Chats base class from rfc-think-multi-session.md will land as sugar over this pattern (~10-line Inbox); 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-chat tests: 459/459 passing
  • @cloudflare/think tests: 249/249 passing
  • agents tests: 1321/1321 passing
  • examples/multi-ai-chat typechecks with the Chat top-level binding removed
  • Smoke-test examples/multi-ai-chat end-to-end in the browser — not blocking CI

Made with Cursor

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 21, 2026

🦋 Changeset detected

Latest commit: 6662d8f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@cloudflare/ai-chat Minor
agents Patch

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

devin-ai-integration[bot]

This comment was marked as resolved.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 21, 2026

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@1353

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@1353

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@1353

hono-agents

npm i https://pkg.pr.new/hono-agents@1353

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@1353

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@1353

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@1353

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@1353

commit: 6662d8f

@threepointone threepointone marked this pull request as draft April 21, 2026 17:02
@threepointone threepointone changed the title ai-chat: align with think + maintenance RFC + multi-ai-chat example wip: ai-chat: align with think + maintenance RFC + multi-ai-chat example Apr 21, 2026
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 threepointone marked this pull request as ready for review April 21, 2026 19:49
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
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
@threepointone threepointone changed the title wip: ai-chat: align with think + maintenance RFC + multi-ai-chat example ai-chat: align with Think + multi-ai-chat example on the sub-agent routing primitive Apr 22, 2026
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
devin-ai-integration[bot]

This comment was marked as resolved.

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
devin-ai-integration[bot]

This comment was marked as resolved.

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
devin-ai-integration[bot]

This comment was marked as resolved.

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.
@threepointone threepointone merged commit f834c81 into main Apr 22, 2026
3 checks passed
@threepointone threepointone deleted the ai-chat-cleanups branch April 22, 2026 15:55
@github-actions github-actions Bot mentioned this pull request Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant