# 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"
  });

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

### Vanilla JavaScript

```typescript
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!"]);
```

## 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)
});
```

### Transport

`useAgent` speaks the Agent protocol over one of two wires. The default,
`"cf-websocket"`, is a hibernating WebSocket managed by PartySocket. The
experimental `"capnweb"` transport carries protocol frames through a single
[Cap'n Web](https://github.com/cloudflare/capnweb) RPC session whose root
also serves the host's callables natively:

```typescript
useAgent({
  agent: "ChatAgent",
  name: "room-123",
  transport: "capnweb" // default: "cf-websocket"
});
```

Identity, state sync, reconnection with backoff, and terminal close codes are
identical on both wires; PartySocket still owns the connection and the
transport only swaps the socket class it instantiates. `call()` and `stub`
differ by design. On `cf-websocket` they send JSON `rpc` frames. On `capnweb`
they invoke the host's `callables` natively on the Cap'n Web session root: a
method that returns an `RpcTarget` hands you a live stub you can keep
calling, a `ReadableStream` result streams, and chained calls pipeline.
`@callable()` decorators on an `Agent` are a JSON-wire feature and are not
available on `capnweb` unless the Agent also passes a `callables` target.
The trade-off is that a Cap'n Web connection does not hibernate: the Durable
Object stays in memory while one is open. `AgentClient` accepts the same
`transport` option.

### 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. Both `useAgent` and `AgentClient` expose a `state` property that tracks the current agent state.

### Reading State

```tsx
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.

### Pushing State Updates

```typescript
// 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()`:

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. `agent.state` updates on the next render (React) or immediately (`AgentClient`)

### Listening for State Changes

For side effects when state changes, use the `onStateUpdate` callback:

```typescript
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.state` directly is simpler than tracking state manually with `onStateUpdate`. Use `onStateUpdate` when you need to trigger side effects on state changes.

### State Flow

```
┌─────────┐                      ┌─────────┐
│ Client  │ ── setState() ────▶  │  Agent  │
│         │                      │         │
│ .state  │ ◀── state update ── │         │
└─────────┘      (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<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"]>>
```

### Streaming Responses

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

```typescript
// Agent-side
import { Agent, callable, type StreamingResponse } from "agents";

export class AIAgent extends Agent {
  @callable({ streaming: true })
  async generateText(stream: StreamingResponse, prompt: string) {
    for (const chunk of ["Generated response for ", 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:

```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<State> = {
  // Required
  agent: string; // Agent class name

  // Optional
  name?: string; // Instance name (default: "default")
  host?: string; // Custom host
  path?: string; // Custom path prefix
  transport?: "cf-websocket" | "capnweb"; // Wire (default: "cf-websocket")

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

### Return Value

```typescript
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 reconnection
```

## Vanilla JS Reference

### AgentClientOptions

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

### AgentClient Methods

```typescript
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", () => {});
```

## 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. `agent.state` is updated automatically — 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.
