Agents SDK v0.8.0: readable state, idempotent schedules, typed AgentClient, and Zod 4
The Agents SDK v0.8.0 adds readable state properties to useAgent and AgentClient, prevents duplicate schedule rows across Durable Object restarts with idempotent scheduling, enables full TypeScript inference on AgentClient calls, and upgrades to Zod 4. Also includes fixes for @cloudflare/ai-chat message serialization and duplicates, and adds TanStack AI integration to @cloudflare/codemode.
The latest release of the Agents SDK ↗ exposes agent state as a readable property, prevents duplicate schedule rows across Durable Object restarts, brings full TypeScript inference to AgentClient, and migrates to Zod 4.
Readable state on useAgent and AgentClient
Both useAgent (React) and AgentClient (vanilla JS) now expose a state property that reflects the current agent state. Previously, reading state required manually tracking it through the onStateUpdate callback.
React (useAgent)
const agent = useAgent({
agent: "game-agent",
name: "room-123",
});
// Read state directly — no separate useState + onStateUpdate needed
return <div>Score: {agent.state?.score}</div>;
// Spread for partial updates
agent.setState({ ...agent.state, score: (agent.state?.score ?? 0) + 10 });const agent = useAgent<GameAgent, GameState>({
agent: "game-agent",
name: "room-123",
});
// Read state directly — no separate useState + onStateUpdate needed
return <div>Score: {agent.state?.score}</div>;
// Spread for partial updates
agent.setState({ ...agent.state, score: (agent.state?.score ?? 0) + 10 });
agent.state is reactive — the component re-renders when state changes from either the server or a client-side setState() call.
Vanilla JS (AgentClient)
const client = new AgentClient({
agent: "game-agent",
name: "room-123",
host: "your-worker.workers.dev",
});
client.setState({ score: 100 });
console.log(client.state); // { score: 100 }const client = new AgentClient<GameAgent>({
agent: "game-agent",
name: "room-123",
host: "your-worker.workers.dev",
});
client.setState({ score: 100 });
console.log(client.state); // { score: 100 }
State starts as undefined and is populated when the server sends the initial state on connect (from initialState) or when setState() is called. Use optional chaining (agent.state?.field) for safe access. The onStateUpdate callback continues to work as before — the new state property is additive.
Idempotent schedule()
schedule() now supports an idempotent option that deduplicates by (type, callback, payload), preventing duplicate rows from accumulating when called in places that run on every Durable Object restart such as onStart().
Cron schedules are idempotent by default. Calling schedule("0 * * * *", "tick") multiple times with the same callback, expression, and payload returns the existing schedule row instead of creating a new one. Pass { idempotent: false } to override.
Delayed and date-scheduled types support opt-in idempotency:
import { Agent } from "agents";
class MyAgent extends Agent {
async onStart() {
// Safe across restarts — only one row is created
await this.schedule(60, "maintenance", undefined, { idempotent: true });
}
}import { Agent } from "agents";
class MyAgent extends Agent {
async onStart() {
// Safe across restarts — only one row is created
await this.schedule(60, "maintenance", undefined, { idempotent: true });
}
}
Two new warnings help catch common foot-guns:
- Calling
schedule()insideonStart()without{ idempotent: true }emits aconsole.warnwith actionable guidance (once per callback; skipped for cron and whenidempotentis set explicitly). - If an alarm cycle processes 10 or more stale one-shot rows for the same callback, the SDK emits a
console.warnand aschedule:duplicate_warningdiagnostics channel event.
Typed AgentClient with call inference and stub proxy
AgentClient now accepts an optional agent type parameter for full type inference on RPC calls, matching the typed experience already available with useAgent.
const client = new AgentClient({
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);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 onStateUpdate is also typed:
const client = new AgentClient({
agent: "my-agent",
host: window.location.host,
onStateUpdate: (state) => {
// state is typed as MyAgent's state type
},
});const client = new AgentClient<MyAgent>({
agent: "my-agent",
host: window.location.host,
onStateUpdate: (state) => {
// state is typed as MyAgent's state type
},
});
Existing untyped usage continues to work without changes. The RPC type utilities (AgentMethods, AgentStub, RPCMethods) are now exported from agents/client for advanced typing scenarios.
agents, @cloudflare/ai-chat, and @cloudflare/codemode now require zod ^4.0.0. Zod v3 is no longer supported.
@cloudflare/ai-chat fixes
- Turn serialization —
onChatMessage()and_reply()work is now queued so user requests, tool continuations, andsaveMessages()never stream concurrently. - Duplicate messages on stop — Clicking stop during an active stream no longer splits the assistant message into two entries.
- Duplicate messages after tool calls — Orphaned client IDs no longer leak into persistent storage.
keepAlive() and keepAliveWhile() are no longer experimental
keepAlive() now uses a lightweight in-memory ref count instead of schedule rows. Multiple concurrent callers share a single alarm cycle. The @experimental tag has been removed from both keepAlive() and keepAliveWhile().
@cloudflare/codemode: TanStack AI integration
A new entry point @cloudflare/codemode/tanstack-ai adds support for TanStack AI's ↗ chat() as an alternative to the Vercel AI SDK's streamText():
import {
createCodeTool,
tanstackTools,
} from "@cloudflare/codemode/tanstack-ai";
import { chat } from "@tanstack/ai";
const codeTool = createCodeTool({
tools: [tanstackTools(myServerTools)],
executor,
});
const stream = chat({ adapter, tools: [codeTool], messages });import { createCodeTool, tanstackTools } from "@cloudflare/codemode/tanstack-ai";
import { chat } from "@tanstack/ai";
const codeTool = createCodeTool({
tools: [tanstackTools(myServerTools)],
executor,
});
const stream = chat({ adapter, tools: [codeTool], messages });
Upgrade
To update to the latest version:
npm i agents@latest @cloudflare/ai-chat@latestSource: original entry ↗
More from Cloudflare
Follow Cloudflare to get its new changes in your feed and email digest.
Cloudflare One Client for macOS 2026.8.2100.0
GA release for macOS Cloudflare One Client with improved split tunnel handling that no longer briefly blocks traffic during reconnects, support for non-RFC 1918 local IPv4 networks, faster connects with lower memory use, and numerous reliability fixes across DNS, reauthentication, and client stability.
Cloudflare One Client for Windows 2026.8.2100.0
This GA release improves split tunnel reliability, adds support for non-RFC 1918 local networks, optimizes connection performance with faster reconnections and lower memory usage, and includes numerous bug fixes for DNS, registration, and network handling. The client now features a service recovery mechanism that automatically restarts on system unlock and better handles large hosts files without blocking traffic.
Cloudflare One Client for Linux 2026.8.2100.0
New GA release for Linux with improved split tunnel handling that no longer briefly blocks traffic during reconnects, support for non-RFC 1918 local IPv4 networks, faster tunnel reconnections, and lower memory usage. Includes numerous stability and reliability fixes for DNS, reconnection behavior, and crash issues.