# 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.
## 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 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](https://docs.partykit.io/reference/partysocket-api/) for reliable connections
## Quick Start
### React
```tsx
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 ;
}
```
### Vanilla JavaScript
```typescript
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's automatically converted from camelCase to kebab-case for the URL:
```typescript
// 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"`:
```typescript
// 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 PartySocket options:
```typescript
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:
```typescript
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 component remounts
## State Synchronization
Agents can maintain state that syncs bidirectionally with all connected clients.
### Receiving State Updates
```typescript
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
```typescript
// 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
```
┌─────────┐ ┌─────────┐
│ Client │ ── setState() ────▶ │ Agent │
│ │ │ │
│ │ ◀── 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](https://developers.cloudflare.com/durable-objects/best-practices/create-durable-object-stubs-and-send-requests/#invoke-rpc-methods) directly on the stub without the decorator.
### Using call()
```typescript
// 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:
```typescript
// 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:
```typescript
import type { MyAgent } from "./agents/my-agent";
const agent = useAgent({
agent: "MyAgent",
name: "instance-1"
});
// Now stub methods are fully typed!
const result = await agent.stub.processData({ input: "test" });
// ^? Awaited>
```
### Streaming Responses
For methods that return `StreamingResponse`, handle chunks as they arrive:
```typescript
// 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);
}
});
```
## HTTP Requests with agentFetch
For one-off requests without maintaining a WebSocket connection:
```typescript
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 |
## React Hook Reference
### UseAgentOptions
```typescript
type UseAgentOptions = {
// 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
| (() => Promise>);
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;
};
```
### Return Value
```typescript
const agent = useAgent(options);
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 - 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 reconnection
```
## Vanilla JS Reference
### AgentClientOptions
```typescript
type AgentClientOptions = {
// Required
agent: string; // Agent class name
host: string; // Worker host
// Optional
name?: string; // Instance name (default: "default")
path?: string; // Custom path prefix
query?: Record;
// Callbacks
onStateUpdate?: (state: State, source: "server" | "client") => void;
};
```
### AgentClient Methods
```typescript
const client = new AgentClient(options);
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 - 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", () => {});
```
## MCP Server Integration
If your agent uses MCP (Model Context Protocol) servers, you can receive updates about their state:
```typescript
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(", ")}`);
}
}
});
```
## Error Handling
### Connection Errors
```typescript
const agent = useAgent({
agent: "MyAgent",
onError: (error) => {
console.error("WebSocket error:", error);
},
onClose: () => {
console.log("Connection closed, will auto-reconnect...");
}
});
```
### RPC Errors
```typescript
try {
const result = await agent.call("riskyMethod", [data]);
} catch (error) {
// Error thrown by the agent method
console.error("RPC failed:", error.message);
}
```
### Streaming Errors
```typescript
await agent.call("streamingMethod", [data], {
onChunk: (chunk) => handleChunk(chunk),
onError: (errorMessage) => {
// Stream-specific error handling
console.error("Stream error:", errorMessage);
}
});
```
## Best Practices
### 1. Use Typed Stubs
```typescript
// Prefer this:
const user = await agent.stub.getUser(id);
// Over this:
const user = await agent.call("getUser", [id]);
```
### 2. Reconnection is Automatic
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 needed.
### 3. Optimize Query Caching
```typescript
// 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
});
```
### 4. Clean Up Connections
In vanilla JS, close connections when done:
```typescript
const client = new AgentClient({ agent: "MyAgent", host: "..." });
// When done:
client.close();
```
React's `useAgent` handles cleanup automatically on unmount.