Client SDK
Connect to agents from any JavaScript runtime — browsers, Node.js, Deno, Bun, or edge functions — using WebSockets or HTTP. The SDK provides real-time state synchronization, RPC method calls, and streaming responses.
The client SDK offers two ways to connect with a WebSocket connection, and one way to make HTTP requests.
| Client | Use Case |
|---|---|
useAgent | React hook with automatic reconnection and state management |
AgentClient | Vanilla JavaScript/TypeScript class for any environment |
agentFetch | HTTP requests when WebSocket is not needed |
All clients provide:
- Bidirectional state sync - Push and receive state updates in real-time
- RPC calls - Call agent methods with typed arguments and return values
- Streaming - Handle chunked responses for AI completions
- Auto-reconnection - Automatic reconnection with exponential backoff
import { useAgent } from "agents/react";
function Chat() { const agent = useAgent({ agent: "ChatAgent", name: "room-123", onStateUpdate: (state) => { console.log("New state:", state); }, });
const sendMessage = async () => { const response = await agent.call("sendMessage", ["Hello!"]); console.log("Response:", response); };
return <button onClick={sendMessage}>Send</button>;}import { useAgent } from "agents/react";
function Chat() { const agent = useAgent({ agent: "ChatAgent", name: "room-123", onStateUpdate: (state) => { console.log("New state:", state); }, });
const sendMessage = async () => { const response = await agent.call("sendMessage", ["Hello!"]); console.log("Response:", response); };
return <button onClick={sendMessage}>Send</button>;}import { AgentClient } from "agents/client";
const client = new AgentClient({ agent: "ChatAgent", name: "room-123", host: "your-worker.your-subdomain.workers.dev", onStateUpdate: (state) => { console.log("New state:", state); },});
// Call a methodconst response = await client.call("sendMessage", ["Hello!"]);import { AgentClient } from "agents/client";
const client = new AgentClient({ agent: "ChatAgent", name: "room-123", host: "your-worker.your-subdomain.workers.dev", onStateUpdate: (state) => { console.log("New state:", state); },});
// Call a methodconst response = await client.call("sendMessage", ["Hello!"]);The agent parameter is your agent class name. It is automatically converted from camelCase to kebab-case for the URL:
// These are equivalent:useAgent({ agent: "ChatAgent" }); // → /agents/chat-agent/...useAgent({ agent: "MyCustomAgent" }); // → /agents/my-custom-agent/...useAgent({ agent: "LOUD_AGENT" }); // → /agents/loud-agent/...// These are equivalent:useAgent({ agent: "ChatAgent" }); // → /agents/chat-agent/...useAgent({ agent: "MyCustomAgent" }); // → /agents/my-custom-agent/...useAgent({ agent: "LOUD_AGENT" }); // → /agents/loud-agent/...The name parameter identifies a specific agent instance. If omitted, defaults to "default":
// Connect to a specific chat roomuseAgent({ agent: "ChatAgent", name: "room-123" });
// Connect to a user's personal agentuseAgent({ agent: "UserAgent", name: userId });
// Uses "default" instanceuseAgent({ agent: "ChatAgent" });// Connect to a specific chat roomuseAgent({ agent: "ChatAgent", name: "room-123" });
// Connect to a user's personal agentuseAgent({ agent: "UserAgent", name: userId });
// Uses "default" instanceuseAgent({ agent: "ChatAgent" });Both useAgent and AgentClient accept connection options:
useAgent({ agent: "ChatAgent", name: "room-123",
// Connection settings host: "my-worker.workers.dev", // Custom host (defaults to current origin) path: "/custom/path", // Custom path prefix
// Query parameters (sent on connection) query: { token: "abc123", version: "2", },
// Event handlers onOpen: () => console.log("Connected"), onClose: () => console.log("Disconnected"), onError: (error) => console.error("Error:", error),});useAgent({ agent: "ChatAgent", name: "room-123",
// Connection settings host: "my-worker.workers.dev", // Custom host (defaults to current origin) path: "/custom/path", // Custom path prefix
// Query parameters (sent on connection) query: { token: "abc123", version: "2", },
// Event handlers onOpen: () => console.log("Connected"), onClose: () => console.log("Disconnected"), onError: (error) => console.error("Error:", error),});For authentication tokens or other async data, pass a function that returns a Promise:
useAgent({ agent: "ChatAgent", name: "room-123",
// Async query - called before connecting query: async () => { const token = await getAuthToken(); return { token }; },
// Dependencies that trigger re-fetching the query queryDeps: [userId],
// Cache TTL for the query result (default: 5 minutes) cacheTtl: 60 * 1000, // 1 minute});useAgent({ agent: "ChatAgent", name: "room-123",
// Async query - called before connecting query: async () => { const token = await getAuthToken(); return { token }; },
// Dependencies that trigger re-fetching the query queryDeps: [userId],
// Cache TTL for the query result (default: 5 minutes) cacheTtl: 60 * 1000, // 1 minute});The query function is cached and only re-called when:
queryDepschangecacheTtlexpires- The WebSocket connection closes (automatic cache invalidation)
- The component remounts
Agents can maintain state that syncs bidirectionally with all connected clients.
Both useAgent and AgentClient expose a state property that reflects the current agent state. It starts as undefined until the first state message is received from the server.
const agent = useAgent({ agent: "GameAgent", name: "game-123" });
// Read the current state at any timeconsole.log("Current score:", agent.state?.score);const agent = useAgent({ agent: "GameAgent", name: "game-123" });
// Read the current state at any timeconsole.log("Current score:", agent.state?.score);With useAgent, state updates trigger a React re-render, so agent.state always reflects the latest value in your JSX. With AgentClient, the state field is updated synchronously on each incoming server broadcast or setState call.
const agent = useAgent({ agent: "GameAgent", name: "game-123", onStateUpdate: (state, source) => { // state: The new state from the agent // source: "server" (agent pushed) or "client" (you pushed) console.log(`State updated from ${source}:`, state); setGameState(state); },});const agent = useAgent({ agent: "GameAgent", name: "game-123", onStateUpdate: (state, source) => { // state: The new state from the agent // source: "server" (agent pushed) or "client" (you pushed) console.log(`State updated from ${source}:`, state); setGameState(state); },});// Update the agent's state from the clientagent.setState({ score: 100, level: 5 });// Update the agent's state from the clientagent.setState({ score: 100, level: 5 });When you call setState():
- The state is sent to the agent over WebSocket
- The agent's
onStateChanged()method is called - The agent broadcasts the new state to all connected clients
- Your
onStateUpdatecallback fires withsource: "client"
sequenceDiagram
participant Client
participant Agent
Client->>Agent: setState()
Agent-->>Client: onStateUpdate (broadcast)
Call methods on your agent that are decorated with @callable().
// Basic callconst result = await agent.call("getUser", [userId]);
// Call with multiple argumentsconst result = await agent.call("createPost", [title, content, tags]);
// Call with no argumentsconst result = await agent.call("getStats");// Basic callconst result = await agent.call("getUser", [userId]);
// Call with multiple argumentsconst result = await agent.call("createPost", [title, content, tags]);
// Call with no argumentsconst result = await agent.call("getStats");The stub property provides a cleaner syntax for method calls:
// Instead of:const user = await agent.call("getUser", ["user-123"]);
// You can write:const user = await agent.stub.getUser("user-123");
// Multiple arguments work naturally:const post = await agent.stub.createPost(title, content, tags);// Instead of:const user = await agent.call("getUser", ["user-123"]);
// You can write:const user = await agent.stub.getUser("user-123");
// Multiple arguments work naturally:const post = await agent.stub.createPost(title, content, tags);For full type safety, pass your Agent class as a type parameter:
const agent = useAgent({ agent: "MyAgent", name: "instance-1",});
// Now stub methods are fully typedconst result = await agent.stub.processData({ input: "test" });import type { MyAgent } from "./agents/my-agent";
const agent = useAgent<MyAgent>({ agent: "MyAgent", name: "instance-1",});
// Now stub methods are fully typedconst result = await agent.stub.processData({ input: "test" });For methods that return StreamingResponse, handle chunks as they arrive:
// Agent-side:class MyAgent extends Agent { @callable({ streaming: true }) async generateText(stream, prompt) { for await (const chunk of llm.stream(prompt)) { await stream.write(chunk); } }}
// Client-side:await agent.call("generateText", [prompt], { onChunk: (chunk) => { // Called for each chunk appendToOutput(chunk); }, onDone: (finalResult) => { // Called when stream completes console.log("Complete:", finalResult); }, onError: (error) => { // Called if streaming fails console.error("Stream error:", error); },});// Agent-side:class MyAgent extends Agent { @callable({ streaming: true }) async generateText(stream: StreamingResponse, prompt: string) { for await (const chunk of llm.stream(prompt)) { await stream.write(chunk); } }}
// Client-side:await agent.call("generateText", [prompt], { onChunk: (chunk) => { // Called for each chunk appendToOutput(chunk); }, onDone: (finalResult) => { // Called when stream completes console.log("Complete:", finalResult); }, onError: (error) => { // Called if streaming fails console.error("Stream error:", error); },});For one-off requests without maintaining a WebSocket connection:
import { agentFetch } from "agents/client";
// GET requestconst response = await agentFetch({ agent: "DataAgent", name: "instance-1", host: "my-worker.workers.dev",});
const data = await response.json();
// POST request with bodyconst response = await agentFetch( { agent: "DataAgent", name: "instance-1", host: "my-worker.workers.dev", }, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "process" }), },);import { agentFetch } from "agents/client";
// GET requestconst response = await agentFetch({ agent: "DataAgent", name: "instance-1", host: "my-worker.workers.dev",});
const data = await response.json();
// POST request with bodyconst response = await agentFetch( { agent: "DataAgent", name: "instance-1", host: "my-worker.workers.dev", }, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "process" }), },);When to use agentFetch vs WebSocket:
Use agentFetch | Use useAgent/AgentClient |
|---|---|
| One-time requests | Real-time updates needed |
| Server-to-server calls | Bidirectional communication |
| Simple REST-style API | State synchronization |
| No persistent connection needed | Multiple RPC calls |
If your agent uses MCP (Model Context Protocol) servers, you can receive updates about their state:
const agent = useAgent({ agent: "AssistantAgent", name: "session-123", onMcpUpdate: (mcpServers) => { // mcpServers is a record of server states for (const [serverId, server] of Object.entries(mcpServers)) { console.log(`${serverId}: ${server.connectionState}`); console.log(`Tools: ${server.tools?.map((t) => t.name).join(", ")}`); } },});const agent = useAgent({ agent: "AssistantAgent", name: "session-123", onMcpUpdate: (mcpServers) => { // mcpServers is a record of server states for (const [serverId, server] of Object.entries(mcpServers)) { console.log(`${serverId}: ${server.connectionState}`); console.log(`Tools: ${server.tools?.map((t) => t.name).join(", ")}`); } },});const agent = useAgent({ agent: "MyAgent", onError: (error) => { console.error("WebSocket error:", error); }, onClose: () => { console.log("Connection closed, will auto-reconnect..."); },});const agent = useAgent({ agent: "MyAgent", onError: (error) => { console.error("WebSocket error:", error); }, onClose: () => { console.log("Connection closed, will auto-reconnect..."); },});try { const result = await agent.call("riskyMethod", [data]);} catch (error) { // Error thrown by the agent method console.error("RPC failed:", error.message);}try { const result = await agent.call("riskyMethod", [data]);} catch (error) { // Error thrown by the agent method console.error("RPC failed:", error.message);}await agent.call("streamingMethod", [data], { onChunk: (chunk) => handleChunk(chunk), onError: (errorMessage) => { // Stream-specific error handling console.error("Stream error:", errorMessage); },});await agent.call("streamingMethod", [data], { onChunk: (chunk) => handleChunk(chunk), onError: (errorMessage) => { // Stream-specific error handling console.error("Stream error:", errorMessage); },});// Prefer this:const user = await agent.stub.getUser(id);
// Over this:const user = await agent.call("getUser", [id]);// Prefer this:const user = await agent.stub.getUser(id);
// Over this:const user = await agent.call("getUser", [id]);The client auto-reconnects and the agent automatically sends the current state on each connection. Your onStateUpdate callback will fire with the latest state — no manual re-sync is needed. If you use an async query function for authentication, the cache is automatically invalidated on disconnect, ensuring fresh tokens are fetched on reconnect.
// For auth tokens that expire hourly:useAgent({ query: async () => ({ token: await getToken() }), cacheTtl: 55 * 60 * 1000, // Refresh 5 min before expiry queryDeps: [userId], // Refresh if user changes});// For auth tokens that expire hourly:useAgent({ query: async () => ({ token: await getToken() }), cacheTtl: 55 * 60 * 1000, // Refresh 5 min before expiry queryDeps: [userId], // Refresh if user changes});In vanilla JS, close connections when done:
const client = new AgentClient({ agent: "MyAgent", host: "..." });
// When done:client.close();const client = new AgentClient({ agent: "MyAgent", host: "..." });
// When done:client.close();React's useAgent handles cleanup automatically on unmount.
type UseAgentOptions<State> = { // Required agent: string; // Agent class name
// Optional name?: string; // Instance name (default: "default") host?: string; // Custom host path?: string; // Custom path prefix
// Query parameters query?: Record<string, string> | (() => Promise<Record<string, string>>); queryDeps?: unknown[]; // Dependencies for async query cacheTtl?: number; // Query cache TTL in ms (default: 5 min)
// Callbacks onStateUpdate?: (state: State, source: "server" | "client") => void; onMcpUpdate?: (mcpServers: MCPServersState) => void; onOpen?: () => void; onClose?: () => void; onError?: (error: Event) => void; onMessage?: (message: MessageEvent) => void;};The useAgent hook returns an object with the following properties and methods:
| Property/Method | Type | Description |
|---|---|---|
agent | string | Kebab-case agent name |
name | string | Instance name |
setState(state) | void | Push state to agent |
call(method, args?, options?) | Promise | Call agent method |
stub | Proxy | Typed method calls |
send(data) | void | Send raw WebSocket message |
close() | void | Close connection |
reconnect() | void | Force reconnection |
type AgentClientOptions<State> = { // Required agent: string; // Agent class name host: string; // Worker host
// Optional name?: string; // Instance name (default: "default") path?: string; // Custom path prefix query?: Record<string, string>;
// Callbacks onStateUpdate?: (state: State, source: "server" | "client") => void;};| Property/Method | Type | Description |
|---|---|---|
agent | string | Kebab-case agent name |
name | string | Instance name |
setState(state) | void | Push state to agent |
call(method, args?, options?) | Promise | Call agent method |
send(data) | void | Send raw WebSocket message |
close() | void | Close connection |
reconnect() | void | Force reconnection |
The client also supports WebSocket event listeners:
client.addEventListener("open", () => {});client.addEventListener("close", () => {});client.addEventListener("error", () => {});client.addEventListener("message", () => {});client.addEventListener("open", () => {});client.addEventListener("close", () => {});client.addEventListener("error", () => {});client.addEventListener("message", () => {});If your chat UI renders retained child runs from Agent tools, use useAgentToolEvents() alongside useAgent() and useAgentChat(). The hook subscribes to the parent connection, replays retained child timelines, and groups runs by parent tool call ID.
import { useAgent, useAgentToolEvents } from "agents/react";import { useAgentChat } from "@cloudflare/ai-chat/react";
const agent = useAgent({ agent: "Assistant", name: userId });const { messages } = useAgentChat({ agent });const agentTools = useAgentToolEvents({ agent });import { useAgent, useAgentToolEvents } from "agents/react";import { useAgentChat } from "@cloudflare/ai-chat/react";
const agent = useAgent({ agent: "Assistant", name: userId });const { messages } = useAgentChat({ agent });const agentTools = useAgentToolEvents({ agent });