Skip to content

refactor(agents): extract state subsystem into agent/state.ts#1887

Open
cjol wants to merge 1 commit into
mainfrom
refactor/extract-state-subsystem
Open

refactor(agents): extract state subsystem into agent/state.ts#1887
cjol wants to merge 1 commit into
mainfrom
refactor/extract-state-subsystem

Conversation

@cjol

@cjol cjol commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

This PR pulls the Agent state machinery out of the monolithic Agent class in packages/agents/src/index.ts and into a dedicated packages/agents/src/agent/state.ts module (AgentState). It is a surface-level extraction: behaviour and the public API are unchanged.

Why

  • index.ts is ~12k lines and the state logic (in-memory cache, SQLite persistence, protocol broadcast, connection readonly/no-protocol flags, state-change hook dispatch) was interleaved with everything else, making it hard for humans and agents to reason about in isolation.
  • Grouping that logic in one documented module shrinks index.ts substantially and gives the state subsystem a single place to read and edit.
  • The extraction uses a narrow AgentStateHost<State> callback interface rather than handing AgentState the whole Agent, so the boundary is explicit instead of "everything can reach everything". Passing the full Agent would have been less churn but would have preserved the coupling this change is meant to expose.
  • This is deliberately not a functional or lower-layer extraction. AgentState still depends on Agent-only facilities, so it is not yet usable on a bare partyserver Server; see the couplings listed below.

Architectural Changes

Before: state cache, persistence, broadcast, connection flags, and hook dispatch lived as methods and fields directly on Agent.

After: Agent holds a private _state: AgentState<State> and delegates to it. AgentState calls back into Agent only through the AgentStateHost<State> interface. AgentStateApi<State> describes the curated user-facing subset that AgentState implements, so a future major could expose the subsystem without leaking internal plumbing.

Still Agent-only (why it is not yet reusable on a lower layer). partyserver already provides ctx, broadcast, getConnections, onError, connections, and hibernation, so those are not blockers. The remaining couplings are:

  • Ambient agent context: state.ts imports the agentContext AsyncLocalStorage directly and uses it for readonly-connection gating and to run the hook so getCurrentAgent() works. This must become host-provided context.
  • Schema ownership: the cf_agents_state table is created by the Agent constructor (not by AgentState), the sql tagged-template helper is an Agent method, and the cf_schema_version migration framework co-inhabits the same table. AgentState is currently a tenant of a table the Agent layer owns.
  • State-sync protocol/client: the live half depends on the CF_AGENT_STATE message type and a client that understands it (useAgent / AgentClient).
  • Typed hook surface: initialState, validateStateChange, onStateChanged / onStateUpdate are Agent's user-facing override points.
  • Observability: _emit("state:update") is Agent's event bus.
  • Connection-flag semantics: the wrapping mechanism is pure partyserver, but the meanings (readonly = block writes, no-protocol = suppress CF_AGENT_* frames) are Agent-layer policy.

Code Changes

  • New AgentState<State> in agent/state.ts owns the state cache, persistence, broadcast, connection-flag machinery, and hook dispatch, talking back to Agent via AgentStateHost<State>.
  • Agent delegates its state methods to _state. Public API (state, setState, setConnectionReadonly, isConnectionReadonly, isConnectionProtocolEnabled) is unchanged and still declared on Agent.prototype; agent.state still returns the state value.
  • Removed five private pass-through wrappers on Agent (_broadcastProtocol, _setStateInternal, _ensureConnectionWrapped, _setConnectionNoProtocol, _cf_getRawConnectionState) and inlined their call sites to _state.
  • Tombstoned _unsafe_getConnectionFlag / _unsafe_setConnectionFlag as @deprecated forwarders with no remaining in-repo callers.
  • Documented the module: a top-level overview of what "state" is, and an expanded explanation of the persistence-hook dispatch mode.
  • Test helpers that force the in-memory cache now go through the renamed private _cachedState accessor.

No changeset is included: AgentStateApi is exported from the module but not from the package's public entry points, and no published behaviour changes.

Compatibility

Nothing user-facing changes. _unsafe_getConnectionFlag / _unsafe_setConnectionFlag remain on the prototype but are now @deprecated internal forwarders slated for removal in a future major; they were never part of the documented API.


Open in Devin Review

Move the Agent state machinery out of the monolithic Agent class in
index.ts and into a dedicated `packages/agents/src/agent/state.ts`
module (`AgentState`). This is a surface-level extraction whose goal is
legibility: index.ts shrinks substantially and the state cache,
persistence, broadcast, connection-flag, and hook-dispatch logic now
live together in one documented place that humans and agents can reason
about without paging through the ~12k-line Agent class.

What changed:

- New `AgentState<State>` owns the in-memory state cache, SQLite
  persistence, protocol broadcast, connection readonly/no-protocol
  flags, and the state-change hook dispatch. It talks back to the Agent
  through a narrow `AgentStateHost<State>` callback interface rather
  than holding the whole Agent.
- `Agent` keeps a private `_state` instance and delegates to it. Public
  API (`state`, `setState`, `setConnectionReadonly`,
  `isConnectionReadonly`, `isConnectionProtocolEnabled`) is unchanged and
  still declared on `Agent.prototype`; `agent.state` still returns the
  state value.
- Added `AgentStateApi<State>`, the curated user-facing subset that
  `AgentState` implements, so a future major could expose the subsystem
  without leaking internal plumbing.
- Deleted five private pass-through wrappers on Agent (`_broadcastProtocol`,
  `_setStateInternal`, `_ensureConnectionWrapped`,
  `_setConnectionNoProtocol`, `_cf_getRawConnectionState`) and inlined
  their call sites to `_state`. Tombstoned `_unsafe_getConnectionFlag`
  / `_unsafe_setConnectionFlag` as `@deprecated` forwarders with no
  remaining in-repo callers.
- Documented the module: a top-level overview of what "state" is,
  and an expanded explanation of the persistence-hook dispatch mode.

Behaviour is unchanged. This is not a semantic/public API change, so no
changeset is included; `AgentStateApi` is exported from the module but
not from the package's public entry points.

What this is NOT yet:

This extraction makes the subsystem easier to read, not usable
independently of the Agent base class. Making `AgentState` work on a
lower layer (e.g. a bare partyserver `Server`) remains a future goal.
partyserver already provides `ctx`, `broadcast`, `getConnections`,
`onError`, connections, and hibernation, so those are not blockers. The
remaining Agent-only couplings are:

- Ambient agent context: state.ts imports the `agentContext`
  AsyncLocalStorage directly and uses it for readonly-connection gating
  and to run the hook so `getCurrentAgent()` works. partyserver has no
  equivalent; this must become host-provided context.
- Schema ownership: the `cf_agents_state` table is created by the Agent
  constructor (not by `AgentState`), the `sql` tagged-template helper is
  an Agent method, and the `cf_schema_version` migration framework
  co-inhabits the same table. `AgentState` is currently a tenant of a
  table the Agent layer owns.
- State-sync protocol/client: the live half depends on the
  `CF_AGENT_STATE` message type and a client that understands it
  (`useAgent` / `AgentClient`), neither of which partyserver provides.
- Typed hook surface: `initialState`, `validateStateChange`,
  `onStateChanged` / `onStateUpdate` are Agent's user-facing override
  points, not partyserver concepts.
- Observability: `_emit("state:update")` is Agent's event bus.
- Connection-flag semantics: the wrapping mechanism is pure partyserver,
  but the meanings (readonly = block writes, no-protocol = suppress
  CF_AGENT_* frames) are Agent-layer policy.

The public export of `AgentState` / `AgentStateApi` is deliberately left
for last, to be added when the subsystem is actually ready to stand on
its own.
@changeset-bot

changeset-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6d65916

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@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 1 potential issue.

Open in Devin Review

Comment on lines +73 to +74
const CF_SUB_AGENT_OUTER_URL_KEY = "_cf_subAgentOuterUrl";
const CF_SUB_AGENT_TAGS_KEY = "_cf_subAgentTags";

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.

🔍 Duplicate constant definitions across state.ts and index.ts

The constants CF_SUB_AGENT_OUTER_URL_KEY and CF_SUB_AGENT_TAGS_KEY are now defined in both packages/agents/src/agent/state.ts:73-74 (where they're used in CF_INTERNAL_KEYS) and remain in packages/agents/src/index.ts (where they're used in sub-agent forwarding code). The string values are identical so there's no behavioral issue, but this duplication is a code smell from an incomplete extraction — if either copy is updated independently, the internal-key filtering and the sub-agent code would silently diverge. Consider exporting these from state.ts and importing them in index.ts, or moving them to a shared constants module.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@pkg-pr-new

pkg-pr-new Bot commented Jul 7, 2026

Copy link
Copy Markdown

Open in StackBlitz

agents

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

@cloudflare/ai-chat

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

@cloudflare/codemode

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

create-think

npm i https://pkg.pr.new/create-think@1887

hono-agents

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

@cloudflare/shell

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

@cloudflare/think

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

@cloudflare/voice

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

@cloudflare/worker-bundler

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

commit: 6d65916

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