refactor(agents): extract facets into a dynamic-agents capability, reposition for isolation not chat sessions - #2193
Merged
Merged
Conversation
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 detectedLatest commit: e76fa00 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: |
- 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
- 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
- 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
- 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
- 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
…ation # Conflicts: # pnpm-lock.yaml
Contributor
There was a problem hiding this comment.
Devin Review found 2 new potential issues.
5 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
ben-reitz
approved these changes
Sep 1, 2026
- 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
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.
What this does
Facets ("sub-agents") had grown into ~2,400 of
index.ts's ~12,150 lines, inlined in theAgentgod 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.dynamicAgentsThe new surface:
this.dynamicAgents.{get,abort,delete,has,list}on everyAgent, plus theDynamicAgentClassandDynamicAgentStubtype names, all marked@experimental.subAgent()/abortSubAgent()/deleteSubAgent()/hasSubAgent()/listSubAgents()are unchanged in behavior,@deprecatedin 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(), andonBeforeSubAgentare untouched. Runtime and type-level tests pin the new facade and typed child stub.3. Docs (
docs/agents/sub-agents.mdrewrite)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-levelChatAgentDO per conversation plus a per-userUserAgentindex 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 isdestroy()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 atexamples/next/chatsfor the many-chats case; it still works and is left as-is.5. Future topology RFC, no implementation
design/rfc-user-chat-durable-objects.mdproposes 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 separateAIChatAgent/Thinkverification. It explicitly does not implement that migration in this PR or propose a frameworkChatsbase class.Verification
packages/agents: fulltest:workersproject green (1976 tests / 114 files), plus the earlierai-chat(655 tests) andthinkpackage suites (both subclassAgentvia protected members)pnpm run checkclean (sherif, exports, formatting, lint, and all 121 TypeScript projects)examples/next/chats: 10 tests green;examples/next/dynamic-agents: 5 tests greenNot in this PR
No existing application or package is migrated from facet-backed chat sessions to top-level Chat DOs.
ThinkandAIChatAgentremain functionally unchanged; inheritedsubAgent()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.