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 component remounts
Agents can maintain state that syncs bidirectionally with all connected clients.
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
onStateUpdate()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.
// 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", () => {});Was this helpful?
- Resources
- API
- New to Cloudflare?
- Directory
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- © 2026 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark
-