releases.shpreview

Agent state now readable; idempotent schedules prevent duplicates

v0.8.0

4 features1 fixThis release4 featuresNew capabilities1 fixBug fixesAI-tallied from the release notes
From the original release noteView original ↗

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)

<span class="line"><span class="nb-shiki-1itgoe">const</span><span class="nb-shiki-dzsirb"> agent</span><span class="nb-shiki-1itgoe"> =</span><span class="nb-shiki-1t8gfj"> useAgent</span><span class="nb-shiki-140thh">({</span></span>
<span class="line"><span class="nb-shiki-140thh">	agent: </span><span class="nb-shiki-mdbnqw">"game-agent"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh">	name: </span><span class="nb-shiki-mdbnqw">"room-123"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh">});</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-21nrsd">// Read state directly — no separate useState + onStateUpdate needed</span></span>
<span class="line"><span class="nb-shiki-1itgoe">return</span><span class="nb-shiki-140thh"> <</span><span class="nb-shiki-17hn0y">div</span><span class="nb-shiki-140thh">>Score: {agent.state?.score}</</span><span class="nb-shiki-17hn0y">div</span><span class="nb-shiki-140thh">>;</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-21nrsd">// Spread for partial updates</span></span>
<span class="line"><span class="nb-shiki-140thh">agent.</span><span class="nb-shiki-1t8gfj">setState</span><span class="nb-shiki-140thh">({ </span><span class="nb-shiki-1itgoe">...</span><span class="nb-shiki-140thh">agent.state, score: (agent.state?.score </span><span class="nb-shiki-1itgoe">??</span><span class="nb-shiki-dzsirb"> 0</span><span class="nb-shiki-140thh">) </span><span class="nb-shiki-1itgoe">+</span><span class="nb-shiki-dzsirb"> 10</span><span class="nb-shiki-140thh"> });</span></span>
<span class="line"><span class="nb-shiki-1itgoe">const</span><span class="nb-shiki-dzsirb"> agent</span><span class="nb-shiki-1itgoe"> =</span><span class="nb-shiki-1t8gfj"> useAgent</span><span class="nb-shiki-140thh"><</span><span class="nb-shiki-1t8gfj">GameAgent</span><span class="nb-shiki-140thh">, </span><span class="nb-shiki-1t8gfj">GameState</span><span class="nb-shiki-140thh">>({</span></span>
<span class="line"><span class="nb-shiki-140thh">	agent: </span><span class="nb-shiki-mdbnqw">"game-agent"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh">	name: </span><span class="nb-shiki-mdbnqw">"room-123"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh">});</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-21nrsd">// Read state directly — no separate useState + onStateUpdate needed</span></span>
<span class="line"><span class="nb-shiki-1itgoe">return</span><span class="nb-shiki-140thh"> <</span><span class="nb-shiki-17hn0y">div</span><span class="nb-shiki-140thh">>Score: {agent.state?.score}</</span><span class="nb-shiki-17hn0y">div</span><span class="nb-shiki-140thh">>;</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-21nrsd">// Spread for partial updates</span></span>
<span class="line"><span class="nb-shiki-140thh">agent.</span><span class="nb-shiki-1t8gfj">setState</span><span class="nb-shiki-140thh">({ </span><span class="nb-shiki-1itgoe">...</span><span class="nb-shiki-140thh">agent.state, score: (agent.state?.score </span><span class="nb-shiki-1itgoe">??</span><span class="nb-shiki-dzsirb"> 0</span><span class="nb-shiki-140thh">) </span><span class="nb-shiki-1itgoe">+</span><span class="nb-shiki-dzsirb"> 10</span><span class="nb-shiki-140thh"> });</span></span>

agent.state is reactive — the component re-renders when state changes from either the server or a client-side setState() call.

Vanilla JS (AgentClient)

<span class="line"><span class="nb-shiki-1itgoe">const</span><span class="nb-shiki-dzsirb"> client</span><span class="nb-shiki-1itgoe"> =</span><span class="nb-shiki-1itgoe"> new</span><span class="nb-shiki-1t8gfj"> AgentClient</span><span class="nb-shiki-140thh">({</span></span>
<span class="line"><span class="nb-shiki-140thh">	agent: </span><span class="nb-shiki-mdbnqw">"game-agent"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh">	name: </span><span class="nb-shiki-mdbnqw">"room-123"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh">	host: </span><span class="nb-shiki-mdbnqw">"your-worker.workers.dev"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh">});</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-140thh">client.</span><span class="nb-shiki-1t8gfj">setState</span><span class="nb-shiki-140thh">({ score: </span><span class="nb-shiki-dzsirb">100</span><span class="nb-shiki-140thh"> });</span></span>
<span class="line"><span class="nb-shiki-140thh">console.</span><span class="nb-shiki-1t8gfj">log</span><span class="nb-shiki-140thh">(client.state); </span><span class="nb-shiki-21nrsd">// { score: 100 }</span></span>
<span class="line"><span class="nb-shiki-1itgoe">const</span><span class="nb-shiki-dzsirb"> client</span><span class="nb-shiki-1itgoe"> =</span><span class="nb-shiki-1itgoe"> new</span><span class="nb-shiki-1t8gfj"> AgentClient</span><span class="nb-shiki-140thh"><</span><span class="nb-shiki-1t8gfj">GameAgent</span><span class="nb-shiki-140thh">>({</span></span>
<span class="line"><span class="nb-shiki-140thh">	agent: </span><span class="nb-shiki-mdbnqw">"game-agent"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh">	name: </span><span class="nb-shiki-mdbnqw">"room-123"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh">	host: </span><span class="nb-shiki-mdbnqw">"your-worker.workers.dev"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh">});</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-140thh">client.</span><span class="nb-shiki-1t8gfj">setState</span><span class="nb-shiki-140thh">({ score: </span><span class="nb-shiki-dzsirb">100</span><span class="nb-shiki-140thh"> });</span></span>
<span class="line"><span class="nb-shiki-140thh">console.</span><span class="nb-shiki-1t8gfj">log</span><span class="nb-shiki-140thh">(client.state); </span><span class="nb-shiki-21nrsd">// { score: 100 }</span></span>

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:

<span class="line"><span class="nb-shiki-1itgoe">import</span><span class="nb-shiki-140thh"> { Agent } </span><span class="nb-shiki-1itgoe">from</span><span class="nb-shiki-mdbnqw"> "agents"</span><span class="nb-shiki-140thh">;</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-1itgoe">class</span><span class="nb-shiki-1t8gfj"> MyAgent</span><span class="nb-shiki-1itgoe"> extends</span><span class="nb-shiki-1t8gfj"> Agent</span><span class="nb-shiki-140thh"> {</span></span>
<span class="line"><span class="nb-shiki-1itgoe">	async</span><span class="nb-shiki-1t8gfj"> onStart</span><span class="nb-shiki-140thh">() {</span></span>
<span class="line"><span class="nb-shiki-21nrsd">		// Safe across restarts — only one row is created</span></span>
<span class="line"><span class="nb-shiki-1itgoe">		await</span><span class="nb-shiki-dzsirb"> this</span><span class="nb-shiki-140thh">.</span><span class="nb-shiki-1t8gfj">schedule</span><span class="nb-shiki-140thh">(</span><span class="nb-shiki-dzsirb">60</span><span class="nb-shiki-140thh">, </span><span class="nb-shiki-mdbnqw">"maintenance"</span><span class="nb-shiki-140thh">, </span><span class="nb-shiki-dzsirb">undefined</span><span class="nb-shiki-140thh">, { idempotent: </span><span class="nb-shiki-dzsirb">true</span><span class="nb-shiki-140thh"> });</span></span>
<span class="line"><span class="nb-shiki-140thh">	}</span></span>
<span class="line"><span class="nb-shiki-140thh">}</span></span>
<span class="line"><span class="nb-shiki-1itgoe">import</span><span class="nb-shiki-140thh"> { Agent } </span><span class="nb-shiki-1itgoe">from</span><span class="nb-shiki-mdbnqw"> "agents"</span><span class="nb-shiki-140thh">;</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-1itgoe">class</span><span class="nb-shiki-1t8gfj"> MyAgent</span><span class="nb-shiki-1itgoe"> extends</span><span class="nb-shiki-1t8gfj"> Agent</span><span class="nb-shiki-140thh"> {</span></span>
<span class="line"><span class="nb-shiki-1itgoe">	async</span><span class="nb-shiki-1t8gfj"> onStart</span><span class="nb-shiki-140thh">() {</span></span>
<span class="line"><span class="nb-shiki-21nrsd">		// Safe across restarts — only one row is created</span></span>
<span class="line"><span class="nb-shiki-1itgoe">		await</span><span class="nb-shiki-dzsirb"> this</span><span class="nb-shiki-140thh">.</span><span class="nb-shiki-1t8gfj">schedule</span><span class="nb-shiki-140thh">(</span><span class="nb-shiki-dzsirb">60</span><span class="nb-shiki-140thh">, </span><span class="nb-shiki-mdbnqw">"maintenance"</span><span class="nb-shiki-140thh">, </span><span class="nb-shiki-dzsirb">undefined</span><span class="nb-shiki-140thh">, { idempotent: </span><span class="nb-shiki-dzsirb">true</span><span class="nb-shiki-140thh"> });</span></span>
<span class="line"><span class="nb-shiki-140thh">	}</span></span>
<span class="line"><span class="nb-shiki-140thh">}</span></span>

Two new warnings help catch common foot-guns:

  • Calling schedule() inside onStart() without { idempotent: true } emits a console.warn with actionable guidance (once per callback; skipped for cron and when idempotent is set explicitly).
  • If an alarm cycle processes 10 or more stale one-shot rows for the same callback, the SDK emits a console.warn and a schedule:duplicate_warning diagnostics 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.

<span class="line"><span class="nb-shiki-1itgoe">const</span><span class="nb-shiki-dzsirb"> client</span><span class="nb-shiki-1itgoe"> =</span><span class="nb-shiki-1itgoe"> new</span><span class="nb-shiki-1t8gfj"> AgentClient</span><span class="nb-shiki-140thh">({</span></span>
<span class="line"><span class="nb-shiki-140thh">	agent: </span><span class="nb-shiki-mdbnqw">"my-agent"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh">	host: window.location.host,</span></span>
<span class="line"><span class="nb-shiki-140thh">});</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-21nrsd">// Typed call — method name autocompletes, args and return type inferred</span></span>
<span class="line"><span class="nb-shiki-1itgoe">const</span><span class="nb-shiki-dzsirb"> value</span><span class="nb-shiki-1itgoe"> =</span><span class="nb-shiki-1itgoe"> await</span><span class="nb-shiki-140thh"> client.</span><span class="nb-shiki-1t8gfj">call</span><span class="nb-shiki-140thh">(</span><span class="nb-shiki-mdbnqw">"getValue"</span><span class="nb-shiki-140thh">);</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-21nrsd">// Typed stub — direct RPC-style proxy</span></span>
<span class="line"><span class="nb-shiki-1itgoe">await</span><span class="nb-shiki-140thh"> client.stub.</span><span class="nb-shiki-1t8gfj">getValue</span><span class="nb-shiki-140thh">();</span></span>
<span class="line"><span class="nb-shiki-1itgoe">await</span><span class="nb-shiki-140thh"> client.stub.</span><span class="nb-shiki-1t8gfj">add</span><span class="nb-shiki-140thh">(</span><span class="nb-shiki-dzsirb">1</span><span class="nb-shiki-140thh">, </span><span class="nb-shiki-dzsirb">2</span><span class="nb-shiki-140thh">);</span></span>
<span class="line"><span class="nb-shiki-1itgoe">const</span><span class="nb-shiki-dzsirb"> client</span><span class="nb-shiki-1itgoe"> =</span><span class="nb-shiki-1itgoe"> new</span><span class="nb-shiki-1t8gfj"> AgentClient</span><span class="nb-shiki-140thh"><</span><span class="nb-shiki-1t8gfj">MyAgent</span><span class="nb-shiki-140thh">>({</span></span>
<span class="line"><span class="nb-shiki-140thh">	agent: </span><span class="nb-shiki-mdbnqw">"my-agent"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh">	host: window.location.host,</span></span>
<span class="line"><span class="nb-shiki-140thh">});</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-21nrsd">// Typed call — method name autocompletes, args and return type inferred</span></span>
<span class="line"><span class="nb-shiki-1itgoe">const</span><span class="nb-shiki-dzsirb"> value</span><span class="nb-shiki-1itgoe"> =</span><span class="nb-shiki-1itgoe"> await</span><span class="nb-shiki-140thh"> client.</span><span class="nb-shiki-1t8gfj">call</span><span class="nb-shiki-140thh">(</span><span class="nb-shiki-mdbnqw">"getValue"</span><span class="nb-shiki-140thh">);</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-21nrsd">// Typed stub — direct RPC-style proxy</span></span>
<span class="line"><span class="nb-shiki-1itgoe">await</span><span class="nb-shiki-140thh"> client.stub.</span><span class="nb-shiki-1t8gfj">getValue</span><span class="nb-shiki-140thh">();</span></span>
<span class="line"><span class="nb-shiki-1itgoe">await</span><span class="nb-shiki-140thh"> client.stub.</span><span class="nb-shiki-1t8gfj">add</span><span class="nb-shiki-140thh">(</span><span class="nb-shiki-dzsirb">1</span><span class="nb-shiki-140thh">, </span><span class="nb-shiki-dzsirb">2</span><span class="nb-shiki-140thh">);</span></span>

State is automatically inferred from the agent type, so onStateUpdate is also typed:

<span class="line"><span class="nb-shiki-1itgoe">const</span><span class="nb-shiki-dzsirb"> client</span><span class="nb-shiki-1itgoe"> =</span><span class="nb-shiki-1itgoe"> new</span><span class="nb-shiki-1t8gfj"> AgentClient</span><span class="nb-shiki-140thh">({</span></span>
<span class="line"><span class="nb-shiki-140thh">	agent: </span><span class="nb-shiki-mdbnqw">"my-agent"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh">	host: window.location.host,</span></span>
<span class="line"><span class="nb-shiki-1t8gfj">	onStateUpdate</span><span class="nb-shiki-140thh">: (</span><span class="nb-shiki-1jdh33">state</span><span class="nb-shiki-140thh">) </span><span class="nb-shiki-1itgoe">=></span><span class="nb-shiki-140thh"> {</span></span>
<span class="line"><span class="nb-shiki-21nrsd">		// state is typed as MyAgent's state type</span></span>
<span class="line"><span class="nb-shiki-140thh">	},</span></span>
<span class="line"><span class="nb-shiki-140thh">});</span></span>
<span class="line"><span class="nb-shiki-1itgoe">const</span><span class="nb-shiki-dzsirb"> client</span><span class="nb-shiki-1itgoe"> =</span><span class="nb-shiki-1itgoe"> new</span><span class="nb-shiki-1t8gfj"> AgentClient</span><span class="nb-shiki-140thh"><</span><span class="nb-shiki-1t8gfj">MyAgent</span><span class="nb-shiki-140thh">>({</span></span>
<span class="line"><span class="nb-shiki-140thh">	agent: </span><span class="nb-shiki-mdbnqw">"my-agent"</span><span class="nb-shiki-140thh">,</span></span>
<span class="line"><span class="nb-shiki-140thh">	host: window.location.host,</span></span>
<span class="line"><span class="nb-shiki-1t8gfj">	onStateUpdate</span><span class="nb-shiki-140thh">: (</span><span class="nb-shiki-1jdh33">state</span><span class="nb-shiki-140thh">) </span><span class="nb-shiki-1itgoe">=></span><span class="nb-shiki-140thh"> {</span></span>
<span class="line"><span class="nb-shiki-21nrsd">		// state is typed as MyAgent's state type</span></span>
<span class="line"><span class="nb-shiki-140thh">	},</span></span>
<span class="line"><span class="nb-shiki-140thh">});</span></span>

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 serializationonChatMessage() and _reply() work is now queued so user requests, tool continuations, and saveMessages() 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():

<span class="line"><span class="nb-shiki-1itgoe">import</span><span class="nb-shiki-140thh"> {</span></span>
<span class="line"><span class="nb-shiki-140thh">	createCodeTool,</span></span>
<span class="line"><span class="nb-shiki-140thh">	tanstackTools,</span></span>
<span class="line"><span class="nb-shiki-140thh">} </span><span class="nb-shiki-1itgoe">from</span><span class="nb-shiki-mdbnqw"> "@cloudflare/codemode/tanstack-ai"</span><span class="nb-shiki-140thh">;</span></span>
<span class="line"><span class="nb-shiki-1itgoe">import</span><span class="nb-shiki-140thh"> { chat } </span><span class="nb-shiki-1itgoe">from</span><span class="nb-shiki-mdbnqw"> "@tanstack/ai"</span><span class="nb-shiki-140thh">;</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-1itgoe">const</span><span class="nb-shiki-dzsirb"> codeTool</span><span class="nb-shiki-1itgoe"> =</span><span class="nb-shiki-1t8gfj"> createCodeTool</span><span class="nb-shiki-140thh">({</span></span>
<span class="line"><span class="nb-shiki-140thh">	tools: [</span><span class="nb-shiki-1t8gfj">tanstackTools</span><span class="nb-shiki-140thh">(myServerTools)],</span></span>
<span class="line"><span class="nb-shiki-140thh">	executor,</span></span>
<span class="line"><span class="nb-shiki-140thh">});</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-1itgoe">const</span><span class="nb-shiki-dzsirb"> stream</span><span class="nb-shiki-1itgoe"> =</span><span class="nb-shiki-1t8gfj"> chat</span><span class="nb-shiki-140thh">({ adapter, tools: [codeTool], messages });</span></span>
<span class="line"><span class="nb-shiki-1itgoe">import</span><span class="nb-shiki-140thh"> { createCodeTool, tanstackTools } </span><span class="nb-shiki-1itgoe">from</span><span class="nb-shiki-mdbnqw"> "@cloudflare/codemode/tanstack-ai"</span><span class="nb-shiki-140thh">;</span></span>
<span class="line"><span class="nb-shiki-1itgoe">import</span><span class="nb-shiki-140thh"> { chat } </span><span class="nb-shiki-1itgoe">from</span><span class="nb-shiki-mdbnqw"> "@tanstack/ai"</span><span class="nb-shiki-140thh">;</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-1itgoe">const</span><span class="nb-shiki-dzsirb"> codeTool</span><span class="nb-shiki-1itgoe"> =</span><span class="nb-shiki-1t8gfj"> createCodeTool</span><span class="nb-shiki-140thh">({</span></span>
<span class="line"><span class="nb-shiki-140thh">	tools: [</span><span class="nb-shiki-1t8gfj">tanstackTools</span><span class="nb-shiki-140thh">(myServerTools)],</span></span>
<span class="line"><span class="nb-shiki-140thh">	executor,</span></span>
<span class="line"><span class="nb-shiki-140thh">});</span></span>
<span class="line"></span>
<span class="line"><span class="nb-shiki-1itgoe">const</span><span class="nb-shiki-dzsirb"> stream</span><span class="nb-shiki-1itgoe"> =</span><span class="nb-shiki-1t8gfj"> chat</span><span class="nb-shiki-140thh">({ adapter, tools: [codeTool], messages });</span></span>
Upgrade

To update to the latest version:

<span class="line"><span class="nb-shiki-1t8gfj">npm</span><span class="nb-shiki-mdbnqw"> i</span><span class="nb-shiki-mdbnqw"> agents@latest</span><span class="nb-shiki-mdbnqw"> @cloudflare/ai-chat@latest</span></span>

Fetched July 22, 2026