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 isn't 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 - Built on PartySocket for reliable connections
import { useAgent } from "agents/react";
function Chat() {
const agent = useAgent({
agent: "ChatAgent",
name: "room-123"
});
const sendMessage = async () => {
const response = await agent.call("sendMessage", ["Hello!"]);
console.log("Response:", response);
};
return (
<div>
<p>Messages: {agent.state?.messageCount ?? 0}</p>
<button onClick={sendMessage}>Send</button>
</div>
);
}import { AgentClient } from "agents/client";
const client = new AgentClient({
agent: "ChatAgent",
name: "room-123",
host: "your-worker.your-subdomain.workers.dev"
});
await client.ready;
// Read state directly
console.log("Current state:", client.state);
// Call a method
const response = await client.call("sendMessage", ["Hello!"]);The agent parameter is your agent class name. It's 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/...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" });Both useAgent and AgentClient accept PartySocket 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)
});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
});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. Both useAgent and AgentClient expose a state property that tracks the current agent state.
const agent = useAgent({
agent: "GameAgent",
name: "game-123"
});
// Read state directly — reactive in React (re-renders on change)
return <div>Score: {agent.state?.score}</div>;agent.state starts as undefined and is populated when the server sends state on connect (from the agent's initialState). Use optional chaining for safe access.
// Update the agent's state from the client
agent.setState({ score: 100, level: 5 });
// Spread existing state for partial updates
agent.setState({ ...agent.state, score: agent.state.score + 10 });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
agent.stateupdates on the next render (React) or immediately (AgentClient)
For side effects when state changes, use the onStateUpdate callback:
const agent = useAgent({
agent: "GameAgent",
name: "game-123",
onStateUpdate: (state, source) => {
// source: "server" (agent pushed) or "client" (you pushed)
console.log(`State updated from ${source}:`, state);
}
});For most use cases, reading
agent.statedirectly is simpler than tracking state manually withonStateUpdate. UseonStateUpdatewhen you need to trigger side effects on state changes.
┌─────────┐ ┌─────────┐
│ Client │ ── setState() ────▶ │ Agent │
│ │ │ │
│ .state │ ◀── state update ── │ │
└─────────┘ (broadcast) └─────────┘
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.
// 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");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);For full type safety, pass your Agent class as a type parameter:
import type { MyAgent } from "./agents/my-agent";
const agent = useAgent<MyAgent, MyAgentState>({
agent: "MyAgent",
name: "instance-1"
});
// Now stub methods are fully typed!
const result = await agent.stub.processData({ input: "test" });
// ^? Awaited<ReturnType<MyAgent["processData"]>>For methods that return StreamingResponse, handle chunks as they arrive:
// Agent-side:
@callable()
async generateText(prompt: string) {
return new StreamingResponse(async (stream) => {
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 request
const response = await agentFetch({
agent: "DataAgent",
name: "instance-1",
host: "my-worker.workers.dev"
});
const data = await response.json();
// POST request with body
const 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 |
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 | null>
| (() => Promise<Record<string, string | null>>);
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;
};const agent = useAgent(options);
agent.state; // State | undefined - Current agent state (reactive)
agent.agent; // string - Kebab-case agent name
agent.name; // string - Instance name
agent.setState(state); // void - Push state to agent
agent.call(method, args?, streamOptions?); // Promise<T> - Call agent method
agent.stub; // Proxy - Typed method calls
agent.send(data); // void - Send raw WebSocket message
agent.close(); // void - Close connection
agent.reconnect(); // void - Force reconnectiontype 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;
};const client = new AgentClient(options);
client.state; // State | undefined - Current agent state
client.agent; // string - Kebab-case agent name
client.name; // string - Instance name
client.setState(state); // void - Push state to agent
client.call(method, args?, streamOptions?); // Promise<T> - Call agent method
client.send(data); // void - Send raw WebSocket message
client.close(); // void - Close connection
client.reconnect(); // void - Force reconnection
// Event listeners (inherited from PartySocket)
client.addEventListener("open", () => {});
client.addEventListener("close", () => {});
client.addEventListener("error", () => {});
client.addEventListener("message", () => {});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: "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);
}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]);The client auto-reconnects and the agent automatically sends the current state on each connection. agent.state is updated automatically — no manual re-sync 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
});In vanilla JS, close connections when done:
const client = new AgentClient({ agent: "MyAgent", host: "..." });
// When done:
client.close();React's useAgent handles cleanup automatically on unmount.