Skip to content

Latest commit

 

History

History
410 lines (300 loc) · 8.67 KB

File metadata and controls

410 lines (300 loc) · 8.67 KB

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


Prerequisites

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

1. Install the Package

npm install agents

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

For Hono applications:

npm install agents hono-agents

2. Create an Agent

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

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:

{
  "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:

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

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

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:

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:

{
  "assets": {
    "binding": "ASSETS"
  }
}

6. Add TypeScript Types

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

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

import { useAgent } from "agents/react";

type CounterState = { count: number };

function CounterWidget() {
  const agent = useAgent<CounterState>({
    agent: "Counter"
  });

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

Vanilla JavaScript

import { AgentClient } from "agents/client";

const agent = new AgentClient({
  agent: "Counter",
  name: "user-123", // Optional: unique instance name
  host: window.location.host,
  onStateUpdate: (state) => {
    // Update the DOM when state changes
    document.getElementById("count").textContent = String(state.count);
  }
});

// Call methods — agent.state is also readable directly
document.getElementById("increment").onclick = () => agent.call("increment");

Adding Multiple Agents

Add more agents by extending the configuration:

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

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

Update wrangler.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:

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:

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:

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:

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:

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

WebSocket connection fails

Ensure your routing passes the response through unchanged:

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