Client SDK

Last updated Sep 15, 2026 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.

Overview

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

Quick start

React

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>;
}

Vanilla JavaScript

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 method
const 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 method
const response = await client.call("sendMessage", ["Hello!"]);

Connecting to agents

Agent naming

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/...

Instance names

The name parameter identifies a specific agent instance. If omitted, defaults to "default":

// Connect to a specific chat room
useAgent({ agent: "ChatAgent", name: "room-123" });

// Connect to a user's personal agent
useAgent({ agent: "UserAgent", name: userId });

// Uses "default" instance
useAgent({ agent: "ChatAgent" });
// Connect to a specific chat room
useAgent({ agent: "ChatAgent", name: "room-123" });

// Connect to a user's personal agent
useAgent({ agent: "UserAgent", name: userId });

// Uses "default" instance
useAgent({ agent: "ChatAgent" });

Connection options

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),
});

Async query parameters

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:

  • queryDeps change
  • cacheTtl expires
  • The WebSocket connection closes (automatic cache invalidation)
  • The component remounts

Automatic cache invalidation on disconnect

When the WebSocket connection closes — whether due to network issues, server restarts, or explicit disconnection — the async query cache is automatically invalidated. This ensures that when the client reconnects, the query function is re-executed to fetch fresh data. This is particularly important for authentication tokens that may have expired during the disconnection period.

State synchronization

Agents can maintain state that syncs bidirectionally with all connected clients.

Reading current state

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 time
console.log("Current score:", agent.state?.score);
const agent = useAgent({ agent: "GameAgent", name: "game-123" });

// Read the current state at any time
console.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.

Receiving state updates

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);
    },
});

Pushing state updates

// Update the agent's state from the client
agent.setState({ score: 100, level: 5 });
// Update the agent's state from the client
agent.setState({ score: 100, level: 5 });

When you call setState():

  1. The state is sent to the agent over WebSocket
  2. The agent's onStateChanged() method is called
  3. The agent broadcasts the new state to all connected clients
  4. Your onStateUpdate callback fires with source: "client"

State flow

sequenceDiagram
    participant Client
    participant Agent
    Client->>Agent: setState()
    Agent-->>Client: onStateUpdate (broadcast)

Calling agent methods (RPC)

Call methods on your agent that are decorated with @callable().

Note

The @callable() decorator is only required for methods called from external runtimes (browsers, other services). When calling from within the same Worker, you can use standard Durable Object RPC directly on the stub without the decorator.

Using call()

// Basic call
const result = await agent.call("getUser", [userId]);

// Call with multiple arguments
const result = await agent.call("createPost", [title, content, tags]);

// Call with no arguments
const result = await agent.call("getStats");
// Basic call
const result = await agent.call("getUser", [userId]);

// Call with multiple arguments
const result = await agent.call("createPost", [title, content, tags]);

// Call with no arguments
const result = await agent.call("getStats");

Using the stub proxy

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);

TypeScript integration

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 typed
const 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 typed
const result = await agent.stub.processData({ input: "test" });

Streaming responses

Mark a callable method as streaming. The framework passes a StreamingResponse as its first argument:

// Agent-side
import { Agent, callable } from "agents";

class MyAgent extends Agent {
    @callable({ streaming: true })
    async generateText(stream, prompt) {
        for await (const chunk of llm.stream(prompt)) {
            stream.send(chunk);
        }
        stream.end();
    }
}

// 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
import { Agent, callable, type StreamingResponse } from "agents";

class MyAgent extends Agent {
    @callable({ streaming: true })
    async generateText(stream: StreamingResponse, prompt: string) {
        for await (const chunk of llm.stream(prompt)) {
            stream.send(chunk);
        }
        stream.end();
    }
}

// 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);
    },
});

HTTP requests with agentFetch

For one-off requests without maintaining a WebSocket connection:

```js import { agentFetch } from "agents/client";

// GET request const response = await agentFetch({ agent: "DataAgent", name: "instance-1", host: "my-worker.workers.dev", });