Skip to content

feat: expose readable state property on useAgent and AgentClient#1152

Merged
threepointone merged 2 commits into
mainfrom
agent-state
Mar 23, 2026
Merged

feat: expose readable state property on useAgent and AgentClient#1152
threepointone merged 2 commits into
mainfrom
agent-state

Conversation

@threepointone
Copy link
Copy Markdown
Contributor

@threepointone threepointone commented Mar 22, 2026

Summary

Closes #1004

Both useAgent (React) and AgentClient (vanilla JS) now expose a state property that tracks the current agent state. Previously, state was write-only via setState() — reading state required manually tracking it through the onStateUpdate callback with a separate useState. This made { ...agent.state, field: newValue } silently spread undefined, producing partial state that replaced the full server state.

  • useAgent: agent.state is reactive — backed by React useState, the component re-renders when state changes from either the server or client-side setState()
  • AgentClient: client.state updates synchronously on setState() and when server broadcasts arrive
  • Type: State | undefined — starts undefined, populated when the server sends state on connect (from initialState) or when setState() is called
  • Backward compatible: onStateUpdate callback continues to work exactly as before — the new property is additive

Before (workaround from the issue)

const [gameState, setGameState] = useState(initialState);

const agent = useAgent({
  agent: "game-agent",
  onStateUpdate: (state) => setGameState(state),
});

// Reads from separate useState, not from agent
return <div>{gameState.score}</div>;

After

const agent = useAgent({
  agent: "game-agent",
});

// Read directly — reactive, no extra useState needed
return <div>{agent.state?.score}</div>;

// Spread works correctly now
agent.setState({ ...agent.state, score: agent.state.score + 10 });

Changes

Core (packages/agents/)

File Change
src/react.tsx Added useState for state tracking in useAgent. Updated all 3 overload signatures to include `state: State
src/client.ts Added `state: State

Tests

File Change
src/react-tests/useAgent.test.tsx 7 new integration tests: initial state from server, client setState tracking, server broadcast tracking, sequential updates, spread partial updates (the key use case from #1004), backward compat with onStateUpdate
src/react-tests/client.test.ts 8 new integration tests: initial undefined state, client setState, server broadcast, spread partial updates, sequential updates, simultaneous callback + property, cross-client state sync
src/tests-d/typed-use-agent.test-d.ts Type assertion: state satisfies {} | undefined
src/tests-d/untyped-use-agent.test-d.ts Same type assertion for untyped overload
src/tests-d/typed-agent-client.test-d.ts Same type assertion for AgentClient

Docs (5 files)

File Change
docs/client-sdk.md Quick start examples show agent.state. State sync section restructured: "Reading State" → "Pushing State Updates" → "Listening for State Changes". Return value and AgentClient methods sections list state.
docs/state.md Client-side sync section updated to show agent.state pattern. Fixed import paths (agents/react not @cloudflare/agents/react).
docs/getting-started.md React and vanilla JS examples simplified — removed useState/onStateUpdate workaround.
docs/adding-to-existing-project.md Same simplification.
docs/webhooks.md Dashboard example simplified.

Examples (11 files)

All examples migrated from useState + onStateUpdate workaround to reading agent.state directly:

Example Pattern
examples/tictactoe const state = agent.state ?? defaultState
examples/github-webhook Replaced repoState refs with agent.state
examples/assistant const agents = agent.state?.agents ?? []
playground/StateDemo Removed useState mirror, kept onStateUpdate for logging
playground/ReadonlyDemo const state = agent.state ?? initialState
playground/SecureDemo Same pattern, kept logging
playground/ReceiveDemo Same pattern, kept logging
playground/RoutingDemo const connectionCount = agent.state?.counter ?? 0
playground/PipelineDemo const lastRun = agent.state?.lastRun ?? null
playground/WorkersDemo Same as PipelineDemo
playground/McpClientDemo const isConnected = !!agent.state?.connectedServer

Design decisions

  1. State not reset on disconnectagent.state retains the last known value during reconnection. The server re-sends authoritative state on reconnect via CF_AGENT_STATE, which overwrites any stale value. This prevents UI flashing. Users can check agent.identified to know if state might be stale.

  2. Optimistic client updates — When setState() is called, agent.state updates immediately before the message is sent to the server. If the server rejects it (readonly connection), onStateUpdateError fires but state is already set. The server will re-send authoritative state, correcting the optimistic value.

  3. State | undefined type — State starts as undefined and is populated when the server sends state on connect. This matches the existing onStateUpdate semantics and is safe with optional chaining (agent.state?.field).

Test plan

  • npm run check — all 65 projects typecheck
  • npm run test — all tests pass
  • npm run test:react — 76 integration tests pass (15 new)
  • npm run test:workers — 747 tests pass
  • Type-level tests verify state type on typed/untyped useAgent and AgentClient
  • Verified built .d.ts files include state: State | undefined in public API

Made with Cursor


Open with Devin

Add a readable `state` property to AgentClient and the object returned by `useAgent`, making client state observable and reactive in React.

Implementation: AgentClient now stores `state` (updated on CF_AGENT_STATE broadcasts and on client setState). The React hook tracks state in local React state (`agent.state`) and updates it on server broadcasts and client setState (causing re-renders). `setState` now updates the local property optimistically.

Other changes: updated docs and many example/demo components to read from `agent.state` (with safe optional chaining and sensible defaults), added unit/integration tests and TypeScript tests to validate behavior and types, and created a changeset describing the minor feature. Backwards compatible: existing `onStateUpdate` callbacks continue to work.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 22, 2026

🦋 Changeset detected

Latest commit: dc45862

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

This PR includes changesets to release 1 package
Name Type
agents Minor

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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Mar 22, 2026

Open in StackBlitz

agents

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

@cloudflare/ai-chat

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

@cloudflare/codemode

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

hono-agents

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

@cloudflare/shell

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

@cloudflare/think

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

@cloudflare/voice

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

@cloudflare/worker-bundler

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

commit: dc45862

devin-ai-integration[bot]

This comment was marked as resolved.

Update client SDK docs to clarify the ordering and timing of setState: the state is sent to the agent, onStateChanged runs, the agent broadcasts the new state, and agent.state updates on the next render for React (or immediately for AgentClient). Add a unit test (useAgent.test.tsx) that verifies agent.state updates on the next React render after setState, ensuring the hook follows the documented semantics.
@threepointone threepointone merged commit 16cc622 into main Mar 23, 2026
2 checks passed
@threepointone threepointone deleted the agent-state branch March 23, 2026 08:30
@github-actions github-actions Bot mentioned this pull request Mar 22, 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.

useAgent missing .state property — docs show it but SDK doesn't implement it

1 participant