Minor Changes
-
#1152
16cc622Thanks @threepointone! - feat: expose readablestateproperty onuseAgentandAgentClientBoth
useAgent(React) andAgentClient(vanilla JS) now expose astateproperty that tracks the current agent state. Previously, state was write-only viasetState()— reading state required manually tracking it through theonStateUpdatecallback.React (useAgent)
const agent = useAgent<GameAgent, GameState>({ agent: "game-agent", name: "room-123", }); // Read state directly — no need for separate useState + onStateUpdate return <div>Score: {agent.state?.score}</div>; // Spread for partial updates — works correctly now agent.setState({ ...agent.state, score: agent.state.score + 10 });
agent.stateis reactive — the component re-renders when state changes from either the server or client-sidesetState().Vanilla JS (AgentClient)
const client = new AgentClient<GameAgent>({ agent: "game-agent", name: "room-123", host: "your-worker.workers.dev", }); // State updates synchronously on setState and server broadcasts client.setState({ score: 100 }); console.log(client.state); // { score: 100 }
Backward compatible
The
onStateUpdatecallback continues to work exactly as before. The newstateproperty is additive — it provides a simpler alternative to manual state tracking for the common case.Type:
State | undefinedState starts as
undefinedand is populated when the server sends state on connect (frominitialState) or whensetState()is called. Use optional chaining (agent.state?.field) for safe access. -
#1154
74a018aThanks @threepointone! - feat: idempotentschedule()to prevent row accumulation across DO restartsschedule()now supports anidempotentoption that deduplicates by(type, callback, payload), preventing duplicate rows from accumulating when called repeatedly (e.g., inonStart()).Cron schedules are idempotent by default. Calling
schedule("0 * * * *", "tick")multiple times with the same callback, cron expression, and payload returns the existing schedule instead of creating a duplicate. Set{ idempotent: false }to override.Delayed and scheduled (Date) types support opt-in idempotency:
async onStart() { // Safe across restarts — only one row exists at a time await this.schedule(60, "maintenance", undefined, { idempotent: true }); }
New warnings for common foot-guns:
schedule()called insideonStart()without{ idempotent: true }now emits aconsole.warnwith actionable guidance (once per callback, skipped for cron and whenidempotentis explicitly set)alarm()processing ≥10 stale one-shot rows for the same callback emits aconsole.warnand aschedule:duplicate_warningdiagnostics channel event
-
#1146
b74e108Thanks @threepointone! - feat: strongly-typedAgentClientwithcallinference andstubproxyAgentClientnow accepts an optional agent type parameter for full type inference on RPC calls, matching the typed experience thatuseAgentalready provides.New: typed
callandstubWhen an agent type is provided,
call()infers method names, argument types, and return types from the agent's methods. A newstubproperty provides a direct RPC-style proxy — call agent methods as if they were local functions:const client = new AgentClient<MyAgent>({ agent: "my-agent", host: window.location.host, }); // Typed call — method name autocompletes, args and return type inferred const value = await client.call("getValue"); // Typed stub — direct RPC-style proxy await client.stub.getValue(); await client.stub.add(1, 2);
State is automatically inferred from the agent type, so
onStateUpdateis also typed:const client = new AgentClient<MyAgent>({ agent: "my-agent", host: window.location.host, onStateUpdate: (state) => { // state is typed as MyAgent's state type }, });
Backward compatible
Existing untyped usage continues to work without changes:
const client = new AgentClient({ agent: "my-agent", host: "..." }); client.call("anyMethod", [args]); // still works client.call<number>("add", [1, 2]); // explicit return type still works client.stub.anyMethod("arg1", 123); // untyped stub also available
The previous
AgentClient<State>pattern is preserved —new AgentClient<{ count: number }>({...})still correctly typesonStateUpdateand leavescall/stubuntyped.Breaking:
callis now an instance property instead of a prototype methodAgentClient.prototype.callno longer exists. Thecallfunction is assigned per-instance in the constructor (via.bind()). This is required for the conditional type system to switch between typed and untyped signatures. Normal usage (client.call(...)) is unaffected, but code that reflects on the prototype or subclasses that overridecallas a method may need adjustment.Shared type utilities
The RPC type utilities (
AgentMethods,AgentStub,RPCMethods, etc.) are now exported fromagents/clientso they can be shared betweenAgentClientanduseAgent, and are available to consumers who need them for advanced typing scenarios. -
#1138
36e2020Thanks @threepointone! - Drop Zod v3 from peer dependency range — now requireszod ^4.0.0. Replace dynamicimport("ai")withz.fromJSONSchema()from Zod 4 for MCP tool schema conversion, removing theairuntime dependency from the agents core. RemoveensureJsonSchema().
Patch Changes
-
#1147
1f85b06Thanks @threepointone! - Replace schedule-based keepAlive with lightweight ref-counted alarmskeepAlive()no longer creates schedule rows or emitsschedule:create/schedule:execute/schedule:cancelobservability events — it uses an in-memory ref count and feeds directly into_scheduleNextAlarm()- multiple concurrent
keepAlive()callers now share a single alarm cycle instead of each creating their own interval schedule row - add
_onAlarmHousekeeping()hook (called on every alarm cycle) for extensions like the fiber mixin to run housekeeping without coupling to the scheduling system - bump internal schema to v2 with a migration that cleans up orphaned
_cf_keepAliveHeartbeatschedule rows from the previous implementation - remove
@experimentalfromkeepAlive()andkeepAliveWhile()