refactor(agents): extract state subsystem into agent/state.ts#1887
Conversation
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.
|
| const CF_SUB_AGENT_OUTER_URL_KEY = "_cf_subAgentOuterUrl"; | ||
| const CF_SUB_AGENT_TAGS_KEY = "_cf_subAgentTags"; |
There was a problem hiding this comment.
🔍 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
agents
@cloudflare/ai-chat
@cloudflare/codemode
create-think
hono-agents
@cloudflare/shell
@cloudflare/think
@cloudflare/voice
@cloudflare/worker-bundler
commit: |
This PR pulls the Agent state machinery out of the monolithic Agent class in
packages/agents/src/index.tsand into a dedicatedpackages/agents/src/agent/state.tsmodule (AgentState). It is a surface-level extraction: behaviour and the public API are unchanged.Why
index.tsis ~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.index.tssubstantially and gives the state subsystem a single place to read and edit.AgentStateHost<State>callback interface rather than handingAgentStatethe 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.AgentStatestill depends on Agent-only facilities, so it is not yet usable on a bare partyserverServer; 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:
Agentholds a private_state: AgentState<State>and delegates to it.AgentStatecalls back intoAgentonly through theAgentStateHost<State>interface.AgentStateApi<State>describes the curated user-facing subset thatAgentStateimplements, 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:state.tsimports theagentContextAsyncLocalStorage directly and uses it for readonly-connection gating and to run the hook sogetCurrentAgent()works. This must become host-provided context.cf_agents_statetable is created by the Agent constructor (not byAgentState), thesqltagged-template helper is an Agent method, and thecf_schema_versionmigration framework co-inhabits the same table.AgentStateis currently a tenant of a table the Agent layer owns.CF_AGENT_STATEmessage type and a client that understands it (useAgent/AgentClient).initialState,validateStateChange,onStateChanged/onStateUpdateare Agent's user-facing override points._emit("state:update")is Agent's event bus.CF_AGENT_*frames) are Agent-layer policy.Code Changes
AgentState<State>inagent/state.tsowns the state cache, persistence, broadcast, connection-flag machinery, and hook dispatch, talking back to Agent viaAgentStateHost<State>.Agentdelegates its state methods to_state. Public API (state,setState,setConnectionReadonly,isConnectionReadonly,isConnectionProtocolEnabled) is unchanged and still declared onAgent.prototype;agent.statestill returns the state value._broadcastProtocol,_setStateInternal,_ensureConnectionWrapped,_setConnectionNoProtocol,_cf_getRawConnectionState) and inlined their call sites to_state._unsafe_getConnectionFlag/_unsafe_setConnectionFlagas@deprecatedforwarders with no remaining in-repo callers._cachedStateaccessor.No changeset is included:
AgentStateApiis 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_setConnectionFlagremain on the prototype but are now@deprecatedinternal forwarders slated for removal in a future major; they were never part of the documented API.