Skip to content

refactor(agents): extract facets into a dynamic-agents capability, reposition for isolation not chat sessions - #2193

Merged
mattzcarey merged 20 commits into
mainfrom
chore/facets-investigation
Sep 1, 2026
Merged

refactor(agents): extract facets into a dynamic-agents capability, reposition for isolation not chat sessions#2193
mattzcarey merged 20 commits into
mainfrom
chore/facets-investigation

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

What this does

Facets ("sub-agents") had grown into ~2,400 of index.ts's ~12,150 lines, inlined in the Agent god class, and the docs positioned them as the recommended way to model many chat sessions per user. Investigation (workerd source, cloudflare-os, and this repo's git history of facet-lifecycle bugs) says otherwise: facets are a colocated isolation primitive — separate isolate, own SQLite, no independent alarms, machine-pinned tree — built for code the parent supervises (dynamically-loaded/generated code, per-run tool agents, sandboxes), not for an open-ended set of independent peers.

1. Extraction (zero behavior change)

Moves the facet machinery into packages/agents/src/dynamic-agents/ — types, identity, RPC bridges, the sub-agent registry, lifecycle routing, resolution/teardown/fiber-recovery, and WebSocket forwarding/virtual connections — behind an explicit host port. Registered as a Lifecycle capability (capabilityId: "dynamic-agents"); its hot paths stay composition-root wired because the capability-runner hook contract can't express request-rewrite-and-continue or post-claim WebSocket forwarding (documented on the class). No wire- or storage-visible identifier changes. index.ts: 12,154 → ~10,480 lines.

2. Public API: this.dynamicAgents

The new surface: this.dynamicAgents.{get,abort,delete,has,list} on every Agent, plus the DynamicAgentClass and DynamicAgentStub type names, all marked @experimental. subAgent() / abortSubAgent() / deleteSubAgent() / hasSubAgent() / listSubAgents() are unchanged in behavior, @deprecated in place, and now delegate through the same capability. This delegation also replatforms inherited Think and AIChatAgent facet usage without changing either package or their chat topology. /sub/ URLs, useAgent({ sub }), parentAgent(), and onBeforeSubAgent are untouched. Runtime and type-level tests pin the new facade and typed child stub.

3. Docs (docs/agents/sub-agents.md rewrite)

Verified workerd semantics section, a corrected claim (frames do not flow directly to the child post-upgrade — every frame wakes the root parent and is forwarded over RPC), and an explicit decision rule: a facet is a child whose code/lifecycle the parent supervises and which must live inside the parent; an independent peer you address by name should be its own top-level Durable Object.

4. Two new examples, both with React + Vite UIs and workers-pool tests

  • examples/next/dynamic-agents — what facets are for: a supervisor stores user-submitted Durable Object code, loads it via Worker Loader, and runs it as a facet with isolated storage, supervised abort, and code upgrades over stable state. The editor loads the selected gadget's stored source before deployment.
  • examples/next/chats — the recommended many-chats topology: one top-level ChatAgent DO per conversation plus a per-user UserAgent index DO. Listing and cross-chat search read only the index; each Chat pushes title/preview metadata best-effort after writes; the User DO assigns deterministic activity order and ignores older delivered timestamps; deletion is destroy() plus one row. The example intentionally treats the index as an eventually consistent derived projection and leaves production idempotency, repair, deletion fencing, and User-gated routing to the separate RFC.

examples/multi-ai-chat's README gets a one-paragraph caveat pointing at examples/next/chats for the many-chats case; it still works and is left as-is.

5. Future topology RFC, no implementation

design/rfc-user-chat-durable-objects.md proposes the follow-up architecture discussed here: one top-level User agent owns the catalog, shared workspace/memory, MCP registry/connections, OAuth callbacks, and cross-chat jobs; one independent top-level Chat agent owns each conversation. A browser normally keeps one User socket plus one socket for the active Chat. Switching chats replaces only the Chat socket. The RFC covers identity, User-gated routing, metadata consistency, deletion, repair, rollout from facet-backed apps, and separate AIChatAgent / Think verification. It explicitly does not implement that migration in this PR or propose a framework Chats base class.

Verification

  • packages/agents: full test:workers project green (1976 tests / 114 files), plus the earlier ai-chat (655 tests) and think package suites (both subclass Agent via protected members)
  • Repo-wide: pnpm run check clean (sherif, exports, formatting, lint, and all 121 TypeScript projects)
  • examples/next/chats: 10 tests green; examples/next/dynamic-agents: 5 tests green

Not in this PR

No existing application or package is migrated from facet-backed chat sessions to top-level Chat DOs. Think and AIChatAgent remain functionally unchanged; inherited subAgent() calls simply delegate through the new capability. The possible User-agent + Chat-agent topology is documentation-only in this PR and requires separate implementation work.


Devin Review

First step of moving the facet (sub-agent) machinery out of the Agent
god class into packages/agents/src/dynamic-agents/. Pure motion: the
moved types, identity helpers, and connection/reply bridges keep their
behavior; index.ts imports them under the old local names. Public
exports (SubAgentClass/SubAgentStub) are unchanged, now aliasing the
module's DynamicAgent* names. No wire- or storage-visible identifier
is renamed.

Claude-Session: https://claude.ai/code/session_01KZ4booD9Pt5jXkRVmjhdb7
…istry

The cf_agents_sub_agents table (identity versioning, has/list/record/
forget) now lives in DynamicAgentRegistry with a two-method SQL host
port. Agent keeps hasSubAgent/listSubAgents overloads and the _cf_
identity entry point as thin delegates. Table and column names are
storage-frozen and unchanged.

Claude-Session: https://claude.ai/code/session_01KZ4booD9Pt5jXkRVmjhdb7
…nto DynamicAgents

Introduces the DynamicAgents class with an explicit host port
(DynamicAgentHostPort) that documents every Agent internal the facet
machinery touches. Moves lifecycle route addressing/traversal, root
alarm-owner resolution, facet-prefix cleanup, facet keepAlive leases,
and the cf_agents_facet_runs row index. Agent keeps all _cf_* RPC
entry points as one-line delegates.

Claude-Session: https://claude.ai/code/session_01KZ4booD9Pt5jXkRVmjhdb7
… into DynamicAgents

resolveSubAgent (the ctx.facets.get bootstrap + identity handshake),
abort/delete, recursive descendant destroy, workflow path invocation,
and the root-side facet fiber recovery scan now live on the
DynamicAgents class; the registry is owned by it. Agent keeps _cf_*
RPC entry points and public methods as delegates, plus a private
_runFacetInitInvocation helper so the module never touches the
invocation context machinery directly.

Claude-Session: https://claude.ai/code/session_01KZ4booD9Pt5jXkRVmjhdb7
… DynamicAgents

The parent-side frame forwarding, facet-side virtual connections,
connection-operation queues and broadcast barrier, /sub/ request
forwarding, stub invocation (single-expression RpcProperty dispatch
preserved verbatim), facet init handshake, and connection hydration
now live on DynamicAgents, with module-owned state (bridge ALS,
virtual-connection map, operation tails) and terse method names
(invoke, invokePath, forward, resolve, delete, init, ...). Agent keeps
every _cf_* RPC entry point as a delegate; calls that subclasses
override (_cf_broadcastToSubAgent, _cf_checkRunFibersForFacet) still
dispatch through the host so overrides keep intercepting. The WS
multiplexing keys move to the module; wire and storage names are
unchanged. Full workers project: 1966 tests green.

Claude-Session: https://claude.ai/code/session_01KZ4booD9Pt5jXkRVmjhdb7
The startup restore of cf_agents_is_facet / cf_agents_facet_name /
cf_agents_parent_path plus best-effort virtual-connection hydration
becomes DynamicAgents.restoreFacetContext(); Agent keeps only the
startup-span wrapper so ordering relative to onStart is unchanged.

Claude-Session: https://claude.ai/code/session_01KZ4booD9Pt5jXkRVmjhdb7
Adds the public dynamic-agents capability surface: an @experimental
this.dynamicAgents accessor exposing get/abort/delete/has/list over
the same machinery as the deprecated subAgent()/abortSubAgent()/
deleteSubAgent()/hasSubAgent()/listSubAgents(), which stay working
with @deprecated pointers. DynamicAgentsInternal now registers with
lifecycle.use() under capabilityId "dynamic-agents" (its hot paths
remain composition-root wired; the class doc explains why the runner
hooks cannot express them). New agents/dynamic-agents subpath export
carries DynamicAgents, DynamicAgentClass, and DynamicAgentStub; the
main entry gains no new surface. Facade behavior pinned against the
legacy API in dynamic-agents-api.test.ts.

Claude-Session: https://claude.ai/code/session_01KZ4booD9Pt5jXkRVmjhdb7
…ide this.dynamicAgents

Drops the agents/dynamic-agents subpath export; this.dynamicAgents on
the Agent class is the only public addition. subAgent/abortSubAgent/
deleteSubAgent/hasSubAgent/listSubAgents now delegate through the
facade so both names are one code path.

Claude-Session: https://claude.ai/code/session_01KZ4booD9Pt5jXkRVmjhdb7
examples/next/chats: the recommended many-chats shape — one top-level
ChatAgent DO per conversation plus a per-user UserAgent index DO that
chats push {title, lastMessage, updatedAt} into. Listing, ordering,
and cross-chat search read only the index; deletion is destroy() plus
one row. Five workers-pool tests pin the pattern.

examples/next/dynamic-agents: what facets are for — a Supervisor agent
stores user-submitted DO code, loads it via Worker Loader, and mounts
it as a facet with its own SQLite. Demonstrates supervised abort with
surviving storage, code upgrades over stable state, capability
confinement (globalOutbound: null), and full teardown. Four tests,
including loader-backed facets under vitest-pool-workers.

Claude-Session: https://claude.ai/code/session_01KZ4booD9Pt5jXkRVmjhdb7
…ynamic-agents

Converts both examples to the mcp-client-style stack: Vite +
@cloudflare/vite-plugin + Tailwind/Kumo, served with 'pnpm run start'.

chats: sidebar lists/searches via one useAgent connection to the
per-user index DO; each open chat gets its own WebSocket straight to
that chat's ChatAgent DO.

dynamic-agents: an editor + invoke panel drives the Supervisor —
create a gadget, edit its code, deploy (aborts the facet, loads the
new class over the same storage), invoke, abort, delete, all visible
in a live log.

Pinned react/react-dom to the exact 19.2.7 the agents workspace
package resolves — pnpm had picked 19.2.8 for these two new packages,
producing two React copies and an Invalid hook call crash in
useAgent/partysocket.

Claude-Session: https://claude.ai/code/session_01KZ4booD9Pt5jXkRVmjhdb7
…ai-chat caveat

- docs/agents/sub-agents.md: full rewrite. Facet semantics section
  (separate isolate, own SQLite, no alarms, depth limit, machine-pinned
  tree, design intent); when-to-use table built on the decision rule
  (facet = parent-supervised child that must live inside the parent;
  independent peer = its own DO); corrects the false claim that WS
  frames flow directly to the child post-upgrade (they don't — every
  frame wakes the root and is forwarded over RPC); this.dynamicAgents
  documented as the primary API with the legacy names as a migration
  table; links to both new examples.
- index.md / long-running-agents.md / agent-tools.md: one-sentence
  repositioning touches, no restructuring.
- examples/multi-ai-chat/README.md: caveat pointing at
  examples/next/chats for the many-chats case.

Claude-Session: https://claude.ai/code/session_01KZ4booD9Pt5jXkRVmjhdb7
…t clean)

- packages/agents/src/index.ts: drop the now-unused isValidParentPath
  import (moved to restoreFacetContext).
- examples/next/chats, examples/next/dynamic-agents: pin react/react-dom
  and vitest to the exact versions the rest of the monorepo uses so
  sherif's multiple-dependency-versions check passes.

Claude-Session: https://claude.ai/code/session_01KZ4booD9Pt5jXkRVmjhdb7
@changeset-bot

changeset-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e76fa00

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

This PR includes changesets to release 2 packages
Name Type
agents Minor
@cloudflare/agent-think 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

pkg-pr-new Bot commented Sep 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

agents

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

@cloudflare/ai-chat

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

@cloudflare/codemode

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

hono-agents

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

@cloudflare/shell

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

@cloudflare/think

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

@cloudflare/voice

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

@cloudflare/worker-bundler

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

commit: e76fa00

- load the selected gadget's stored source before deploying edits
- order chat activity in the User DO and prevent delayed pushes from recreating deleted rows
- remove dead extraction delegates and pin the public Agent.dynamicAgents type surface
- add a proposed RFC for a User hub plus independent top-level Chat DOs; no Think or AIChatAgent topology change in this PR
devin-ai-integration[bot]

This comment was marked as resolved.

- key message writes by caller-supplied ids so retries do not duplicate them
- project complete chat snapshots with monotonic revisions and ignore stale delivery
- keep message acceptance independent from index availability and provide pull repair
- cover failed delivery, repair, stale snapshots, deletion, and stable retry results
devin-ai-integration[bot]

This comment was marked as resolved.

- accept messages idempotently by caller-supplied id
- project complete revision-fenced snapshots and repair stale index rows by pull
- gate browser routes through the User catalog instead of exposing physical Agent names
- mark catalog rows deleting before Chat destruction and refuse stale-handle writes
- keep Chat activity time primary, using User sequence only for deterministic ties
devin-ai-integration[bot]

This comment was marked as resolved.

- remove idempotency, repair, gated-routing, and deletion protocols from the example
- document its User index as a best-effort eventually consistent projection
- keep production consistency concerns in the separate RFC
- retain deterministic User ordering and fix colon-containing user ids
devin-ai-integration[bot]

This comment was marked as resolved.

- remove idempotency, repair, gated-routing, and deletion protocols from the example
- document its User index as a best-effort derived projection
- keep production consistency concerns in the separate RFC
- retain deterministic ordering, reject older metadata timestamps, and support colon-containing user ids

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 new potential issues.

5 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment thread examples/next/chats/src/index.ts Outdated
Comment thread examples/next/chats/src/index.ts

@ben-reitz ben-reitz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the architectural change lgtm!

Comment thread packages/agents/src/index.ts
Comment thread .changeset/dynamic-agents-capability.md Outdated
- export DynamicAgentClass and DynamicAgentStub alongside legacy aliases
- mark the new public Agent.dynamicAgents surface as a minor release
- order chat lists by activity time with deterministic receipt-order ties
- cover delayed cross-DO metadata delivery
@mattzcarey
mattzcarey merged commit 87bd594 into main Sep 1, 2026
16 checks passed
@mattzcarey
mattzcarey deleted the chore/facets-investigation branch September 1, 2026 15:41
@github-actions github-actions Bot mentioned this pull request Sep 1, 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.

2 participants