Skip to content

Migrate facet bootstrap to explicit FacetStartupOptions.id#1393

Merged
threepointone merged 8 commits into
mainfrom
facet-explicit-id-bootstrap
Apr 26, 2026
Merged

Migrate facet bootstrap to explicit FacetStartupOptions.id#1393
threepointone merged 8 commits into
mainfrom
facet-explicit-id-bootstrap

Conversation

@threepointone
Copy link
Copy Markdown
Contributor

@threepointone threepointone commented Apr 26, 2026

Summary

Migrates facet bootstrap in Agent from the __ps_name storage-write hack to the documented Cloudflare facet API: pass id: parentNs.idFromName(name) to ctx.facets.get() so the facet gets its own ctx.id.name. PartyServer's existing 0.5.x name getter then resolves this.name correctly without any override mechanism, storage write, or cold-wake hydrate cost.

Closes #1385.

Background

partyserver 0.5.0 made this.name resolve from ctx.id.name natively, eliminating the need for setName() round-trips for normal idFromName/getByName DOs. That change initially missed facets — but for a different reason than #1385 originally diagnosed.

The original issue assumed facets have ctx.id.name === undefined and proposed that setName(facetName) should be the bootstrap primitive. Investigation showed that's wrong: facets spawned via ctx.facets.get(name, factory) without an explicit id in FacetStartupOptions inherit the parent DO's ctx.id — including ctx.id.name. So on a facet, ctx.id.name was the parent's name, and this.name silently misreported. With partyserver 0.5.x's strict setName() guard (throws on ctx.id.name mismatch), the proposed setName(facetName) migration would have failed immediately.

The actual fix lives at the call site, not in partyserver. FacetStartupOptions.id is optional and documented — when supplied, the facet sees that id as its own ctx.id. Pass id: ctx.exports[ParentClassName].idFromName(facetName) to ctx.facets.get() and the facet gets a real ctx.id.name === facetName. Partyserver's existing name getter then does the right thing automatically.

See:

  • cloudflare/partykit#386 (partyserver 0.5.3) — docs and end-to-end test coverage of the explicit-id facet pattern. Zero runtime change.
  • #1385 — original investigation and updated comment with current findings.

What ships

This PR is split across two commits for review clarity:

Commit 1 — chore(deps): bump partyserver to ^0.5.3 and roll up other dep updates

  • Bumps partyserver from ^0.4.1 to ^0.5.3 across the workspace.
  • Rolls up other dep updates that landed during this maintenance pass (@cloudflare/vite-plugin, @cloudflare/vitest-pool-workers, @cloudflare/workers-types, @openai/agents, @vitest/*, nx).
  • No code changes; just package.json + package-lock.json.
  • Non-breaking on its own — partyserver 0.5.3 is byte-identical to 0.5.2 at runtime.

Commit 2 — Migrate facet bootstrap to explicit FacetStartupOptions.id

Code changes (packages/agents/src/index.ts):

  • _cf_resolveSubAgent now passes id: parentNs.idFromName(name) where parentNs = ctx.exports[this.constructor.name]. The parent agent is always bound (it's the entry-point DO), so its namespace is always available via ctx.exports. The id is opaque + a name; nothing routes through the namespace at runtime for facets.
  • _cf_initAsFacet drops the __ps_name storage write entirely and drops the setName(name) call. Just persists cf_agents_is_facet and cf_agents_parent_path, then __unsafe_ensureInitialized() to fire onStart().
  • Adds a defensive this.name !== name guard inside _cf_initAsFacet so any future bug in the parent's id construction surfaces with a clear error instead of silently mis-identifying the facet.
  • New error path: if the parent class isn't bound as a DO namespace, throw a descriptive error pointing at wrangler.jsonc. If this.constructor.name looks minified (heuristic regex ^_*[a-z][a-z0-9]{0,2}$), append a bundler-config hint (e.g. esbuild keepNames: true).
  • alarm() docstring: updated to reflect that this.name resolves from ctx.id.name on the happy path (including for facets).

Test coverage:

  • New error-path tests in tests/sub-agent.test.ts plus TestUnboundParentAgent and TestMinifiedNameParentAgent fixtures (parent classes exported under a different name from their declaration). Both error paths now have regression guards.
  • Existing test "should set this.name to the facet name" already covers the happy path; it would have caught the original bug under partyserver 0.5.x with the old __ps_name approach.

MCP test cleanup:

  • Removes vestigial setName("default") + explicit onStart() call pairs from create-oauth-provider, oauth2-mcp-client, and wait-connections-e2e tests. These were originally needed for partyserver 0.4.x bootstrap but became actual ctx.id.name mismatches under 0.5.x's strict guard. Renamed setName args to match idFromName names where the test addresses by name.

Test plan

  • npx vitest run --project workers passes — 1008 tests, 7 skipped, 0 failures (up from 1006 — the 2 new error-path tests)
  • npm run check passes — 75 typecheck projects green, format/lint clean, exports valid
  • Targeted sub-agent / alarm / MCP tests (139 tests across 8 files) verified independently
  • Validated against published partyserver@0.5.3 from npm
  • Reviewer to spot-check the new error message wording

Made with Cursor


Open in Devin Review

Bumps partyserver from ^0.4.1 to ^0.5.3 across the workspace (root and
all examples / experimental / packages / sites that referenced it),
plus a sweep of other dependency updates that landed during this
maintenance pass:

- @cloudflare/vite-plugin: 1.33.0 -> 1.33.2
- @cloudflare/vitest-pool-workers: 0.14.8 -> 0.15.0
- @cloudflare/workers-types: 4.20260420.1 -> 4.20260425.1
- @openai/agents: 0.8.4 -> 0.8.5
- @vitest/{browser,browser-playwright,runner}: 4.1.4 -> 4.1.5
- nx: 22.6.5 -> 22.7.0
- (plus consumer-side bumps in examples/* and experimental/*)

The partyserver bump is the load-bearing one: 0.5.3 ships docs and
test coverage for using PartyServer with Durable Object Facets via
explicit FacetStartupOptions.id, which the next commit consumes in
the agents framework itself. Runtime behavior of partyserver is
unchanged from 0.5.2, so this commit alone is non-breaking.

No code changes; just package.json + package-lock.json.

Made-with: Cursor
Drops the __ps_name storage write and setName() bootstrap from
_cf_initAsFacet in favor of the documented Cloudflare facet API:
pass id: parentNs.idFromName(name) to ctx.facets.get() so the facet
gets its own ctx.id.name. PartyServer's existing 0.5.x name getter
then resolves this.name correctly without any override mechanism,
storage write, or cold-wake hydrate cost.

Background: facets spawned without an explicit id inherit the parent
DO's ctx.id, so on a facet ctx.id.name was the parent's name, and
this.name silently misreported. The fix lives at the call site, not
in partyserver. See #1385 for the full investigation
and cloudflare/partykit#386 for the partyserver-side documentation.

Changes:

- _cf_resolveSubAgent: pass id: parentNs.idFromName(name) where
  parentNs = ctx.exports[this.constructor.name]. The parent agent
  is always bound (it's the entry-point DO), so its namespace is
  always available via ctx.exports. The id is opaque + a name;
  nothing routes through the namespace at runtime for facets.

- _cf_initAsFacet: drops the __ps_name storage write entirely;
  drops the setName(name) call. Just persists cf_agents_is_facet
  and cf_agents_parent_path, then __unsafe_ensureInitialized() to
  fire onStart(). Adds a defensive `this.name !== name` guard so
  any future bug in the parent's id construction surfaces with a
  clear error instead of silently mis-identifying the facet.

- alarm() docstring: updated to reflect that this.name resolves
  from ctx.id.name on the happy path (including for facets).

- New error-path coverage: if the parent class isn't bound as a DO
  namespace, throw a descriptive error pointing at wrangler.jsonc.
  If this.constructor.name looks minified (heuristic regex), append
  a hint about preserving class names (e.g. esbuild keepNames).

- New tests: TestUnboundParentAgent and TestMinifiedNameParentAgent
  fixtures (parent classes exported under a different name from
  their declaration) exercise both error paths. Existing test "should
  set this.name to the facet name" already covers the happy path.

- MCP test cleanup: removes vestigial setName("default") + onStart()
  call pairs from create-oauth-provider, oauth2-mcp-client, and
  wait-connections-e2e tests. These were originally needed for
  partyserver 0.4.x bootstrap but became actual ctx.id.name
  mismatches under partyserver 0.5.x's strict guard. Renamed setName
  args to match idFromName names where the test addresses by name.

All 1008 tests pass (up from 1006 — the 2 new error-path tests).
The full workers test project, all sub-agent tests, alarms tests,
and MCP tests are green against the published partyserver 0.5.3.

Closes #1385

Made-with: Cursor
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 26, 2026

🦋 Changeset detected

Latest commit: 3fef2aa

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

This PR includes changesets to release 1 package
Name Type
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

Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 5 additional findings.

Open in Devin Review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 26, 2026

Open in StackBlitz

agents

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

@cloudflare/ai-chat

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

@cloudflare/codemode

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

hono-agents

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

@cloudflare/shell

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

@cloudflare/think

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

@cloudflare/voice

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

@cloudflare/worker-bundler

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

commit: 3fef2aa

Adds a parent-class requirements note to docs/sub-agents.md describing
the implicit contract that the parent must be bound as a DO namespace
and have its class name preserved by the bundler. This makes the new
unbound-parent / minified-name error path discoverable in the docs.

Also clarifies the hibernation note (this.name is now carried by
ctx.id, not hydrated from storage), and fixes a stale comment in the
abort-and-re-access test that still referenced `__ps_name` (now only
the agent-specific keys are written by _cf_initAsFacet).

Made-with: Cursor
Per code review feedback on PR #1393: the previous type
`Record<string, DurableObjectClass>` was too narrow and didn't
explain why `ctx.exports[parentClassName].idFromName(...)` works
for facet-only classes that aren't bound in
`durable_objects.bindings` (e.g. `OuterSubAgent` spawning
`InnerSubAgent` in the test fixtures).

The workerd runtime contract is: any class registered via
`migrations.new_sqlite_classes` (or `new_classes`) — bindings or
not — is exposed in ctx.exports as both a `DurableObjectClass` AND
a `DurableObjectNamespace`. The intersection is what makes the
explicit-id facet bootstrap work for nested sub-agents.

Updates the FacetCapableCtx.exports type to
`Record<string, (DurableObjectClass & DurableObjectNamespace) | undefined>`
with a docstring explaining the runtime semantics, and drops the
now-redundant `as unknown as DurableObjectNamespace | undefined`
cast at the call site in `_cf_resolveSubAgent`.

Empirically verified: 47/47 sub-agent tests still pass, all 75
typecheck projects green.

Made-with: Cursor
Drops our hand-rolled inline facets shape in favor of
DurableObjectFacets from @cloudflare/workers-types now that the bumped
peer (^4.20260425.1) ships it natively. Also adds a top-of-interface
docstring explaining why FacetCapableCtx exists at all (we widen
ctx.exports from the MainModule-keyed Cloudflare.Exports to a generic
Record because we can't see the consumer worker's main module from
inside this library).

No runtime change. 79/79 sub-agent tests pass, all 75 typecheck
projects green.

Made-with: Cursor
@threepointone threepointone merged commit 5aaf7c4 into main Apr 26, 2026
2 checks passed
@threepointone threepointone deleted the facet-explicit-id-bootstrap branch April 26, 2026 12:36
@github-actions github-actions Bot mentioned this pull request Apr 24, 2026
threepointone added a commit that referenced this pull request Apr 26, 2026
Add a new section summarizing four post-#1384 maintenance PRs (#1393#1396) and their effects on the multi-session assistant plan. Notes include facet bootstrap via explicit FacetStartupOptions.id (#1393) which removes the storage write/setName shim and makes MyAssistant.name resolve natively; the new beforeStep hook and TurnConfig.output passthrough (#1394); SubmitConcurrencyController being moved into agents/chat (#1395); and message-reconciler moved into agents/chat with Think now reconciling incoming messages (#1396). Clarifies that the chat-shared-layer has been incrementally hoisted into agents/chat and highlights the lack of a vitest+workers harness for examples/assistant, recommending a minimal test harness before hoisting useAgentChat.
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.

Migrate facet bootstrap to partyserver setName() (drops direct __ps_name storage write)

1 participant