branch:
adding-to-existing-project.md
8868 bytesRaw
# Adding Agents to an Existing Project

This guide shows how to add agents to an existing Cloudflare Workers project. If you're starting fresh, see [Getting Started](./getting-started.md) instead.

---

## Prerequisites

- An existing Cloudflare Workers project with `wrangler.jsonc`
- Node.js 18+

---

## 1. Install the Package

```bash
npm install agents
```

For React applications, no additional packages are needed—React bindings are included.

For Hono applications:

```bash
npm install agents hono-agents
```

---

## 2. Create an Agent

Create a new file for your agent (e.g., `src/agents/counter.ts`):

```typescript
import { Agent } from "agents";

type CounterState = {
  count: number;
};

export class Counter extends Agent<Env, CounterState> {
  initialState: CounterState = { count: 0 };

  increment() {
    this.setState({ count: this.state.count + 1 });
    return this.state.count;
  }

  decrement() {
    this.setState({ count: this.state.count - 1 });
    return this.state.count;
  }
}
```

---

## 3. Update wrangler.jsonc

Add the Durable Object binding and migration:

```jsonc
{
  "name": "my-existing-project",
  "main": "src/index.ts",
  "compatibility_date": "2025-01-01",
  "compatibility_flags": ["nodejs_compat"], // Required for agents

  // Add this section
  "durable_objects": {
    "bindings": [
      {
        "name": "Counter",
        "class_name": "Counter"
      }
    ]
  },

  // Add this section
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["Counter"]
    }
  ]
}
```

**Key points:**

- `name` in bindings becomes the property on `env` (e.g., `env.Counter`)
- `class_name` must match your exported class name exactly
- `new_sqlite_classes` enables SQLite storage for state persistence
- The `nodejs_compat` flag is required for the agents package

---

## 4. Export the Agent Class

Your agent class must be exported from your main entry point. Update your `src/index.ts`:

```typescript
// Export the agent class (required for Durable Objects)
export { Counter } from "./agents/counter";

// Your existing exports...
export default {
  // ...
};
```

---

## 5. Wire Up Routing

Choose the approach that matches your project structure:

### Plain Workers (fetch handler)

```typescript
import { routeAgentRequest } from "agents";
export { Counter } from "./agents/counter";

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    // Try agent routing first
    const agentResponse = await routeAgentRequest(request, env);
    if (agentResponse) return agentResponse;

    // Your existing routing logic
    const url = new URL(request.url);
    if (url.pathname === "/api/hello") {
      return Response.json({ message: "Hello!" });
    }

    return new Response("Not found", { status: 404 });
  }
};
```

### Hono

```typescript
import { Hono } from "hono";
import { agentsMiddleware } from "hono-agents";
export { Counter } from "./agents/counter";

const app = new Hono<{ Bindings: Env }>();

// Add agents middleware - handles WebSocket upgrades and agent HTTP requests
app.use("*", agentsMiddleware());

// Your existing routes continue to work
app.get("/api/hello", (c) => c.json({ message: "Hello!" }));

export default app;
```

### With Static Assets

If you're serving static assets alongside agents:

```typescript
import { routeAgentRequest } from "agents";
export { Counter } from "./agents/counter";

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext) {
    // Try agent routing first
    const agentResponse = await routeAgentRequest(request, env);
    if (agentResponse) return agentResponse;

    // Fall back to static assets
    return env.ASSETS.fetch(request);
  }
};
```

Make sure your `wrangler.jsonc` has the assets binding:

```jsonc
{
  "assets": {
    "binding": "ASSETS"
  }
}
```

---

## 6. Add TypeScript Types

Update your `Env` type to include the agent namespace. Create or update `env.d.ts`:

```typescript
import type { Counter } from "./agents/counter";

interface Env {
  // Your existing bindings
  MY_KV: KVNamespace;
  MY_DB: D1Database;

  // Add agent bindings
  Counter: DurableObjectNamespace<Counter>;
}
```

---

## 7. Connect from the Frontend

### React

```tsx
import { useState } from "react";
import { useAgent } from "agents/react";

type CounterState = { count: number };

function CounterWidget() {
  const [count, setCount] = useState(0);

  const agent = useAgent<CounterState>({
    agent: "Counter",
    onStateUpdate: (state) => setCount(state.count)
  });

  return (
    <div>
      <span>{count}</span>
      <button onClick={() => agent.stub.increment()}>+</button>
      <button onClick={() => agent.stub.decrement()}>-</button>
    </div>
  );
}
```

### Vanilla JavaScript

```typescript
import { AgentClient } from "agents/client";

const agent = new AgentClient({
  agent: "Counter",
  name: "user-123", // Optional: unique instance name
  onStateUpdate: (state) => {
    document.getElementById("count").textContent = state.count;
  }
});

// Call methods
document.getElementById("increment").onclick = () => agent.call("increment");
```

---

## Adding Multiple Agents

Add more agents by extending the configuration:

```typescript
// src/agents/chat.ts
export class Chat extends Agent<Env, ChatState> {
  // ...
}

// src/agents/scheduler.ts
export class Scheduler extends Agent<Env> {
  // ...
}
```

Update `wrangler.jsonc`:

```jsonc
{
  "durable_objects": {
    "bindings": [
      { "name": "Counter", "class_name": "Counter" },
      { "name": "Chat", "class_name": "Chat" },
      { "name": "Scheduler", "class_name": "Scheduler" }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["Counter", "Chat", "Scheduler"]
    }
  ]
}
```

Export all agents from your entry point:

```typescript
export { Counter } from "./agents/counter";
export { Chat } from "./agents/chat";
export { Scheduler } from "./agents/scheduler";
```

---

## Common Integration Patterns

### Agents Behind Authentication

Check auth before routing to agents:

```typescript
export default {
  async fetch(request: Request, env: Env) {
    // Check auth for agent routes
    if (request.url.includes("/agents/")) {
      const authResult = await checkAuth(request, env);
      if (!authResult.valid) {
        return new Response("Unauthorized", { status: 401 });
      }
    }

    const agentResponse = await routeAgentRequest(request, env);
    if (agentResponse) return agentResponse;

    // ... rest of routing
  }
};
```

### Custom Agent Path Prefix

By default, agents are routed at `/agents/{agent-name}/{instance-name}`. You can customize this:

```typescript
import { routeAgentRequest } from "agents";

const agentResponse = await routeAgentRequest(request, env, {
  prefix: "/api/agents" // Now routes at /api/agents/{agent-name}/{instance-name}
});
```

### Accessing Agents from Server Code

You can interact with agents directly from your Worker code:

```typescript
import { getAgentByName } from "agents";

export default {
  async fetch(request: Request, env: Env) {
    if (request.url.endsWith("/api/increment")) {
      // Get a specific agent instance
      const counter = await getAgentByName(env.Counter, "shared-counter");
      const newCount = await counter.increment();
      return Response.json({ count: newCount });
    }
    // ...
  }
};
```

---

## Troubleshooting

### "Agent not found" or 404 errors

1. **Check the export** - Agent class must be exported from your main entry point
2. **Check the binding** - `class_name` in `wrangler.jsonc` must match the exported class name exactly
3. **Check the route** - Default route is `/agents/{agent-name}/{instance-name}`

### "No such Durable Object class" error

Add the migration to `wrangler.jsonc`:

```jsonc
"migrations": [
  {
    "tag": "v1",
    "new_sqlite_classes": ["YourAgentClass"]
  }
]
```

### WebSocket connection fails

Ensure your routing passes the response through unchanged:

```typescript
// ✅ Correct - return the response directly
const agentResponse = await routeAgentRequest(request, env);
if (agentResponse) return agentResponse;

// ❌ Wrong - don't wrap or modify the response
const agentResponse = await routeAgentRequest(request, env);
if (agentResponse) return new Response(agentResponse.body); // Breaks WebSocket
```

### State not persisting

Check that:

1. You're using `this.setState()`, not mutating `this.state` directly
2. The agent class is in `new_sqlite_classes` in migrations
3. You're connecting to the same agent instance name

---

## Next Steps

- [State Management](./state.md) - Deep dive into agent state
- [Scheduling](./scheduling.md) - Background tasks and cron jobs
- [Agent Class](./agent-class.md) - Full lifecycle and methods
- [Client SDK](./client-sdk.md) - Complete client API reference