Agents | Koze | Primitives Docs

Agents

Build AI agents with singular convention-based discovery

Agents

Koze auto-discovers .agent.ts files in src/server/. Each file becomes a first-class Worker export.

// src/server/session.agent.ts
import { Agent } from 'agents';

export class SessionAgent extends Agent {
  async onRequest(request: Request) {
    return Response.json({ ok: true });
  }
}

Naming Convention

Agent files use the .agent.ts suffix (singular). Plural forms such as .agents.ts are rejected at dev/build time with a fix-it message.

File Export
session.agent.ts SessionAgent
chat.agent.ts ChatAgent
assistant.agent.ts AssistantAgent

Agent Lifecycle

Agents extend the base Agent class and implement lifecycle methods:

import { Agent } from 'agents';

export class ChatAgent extends Agent {
  // Called on each request
  async onRequest(request: Request) {
    const url = new URL(request.url);
    
    if (url.pathname === '/chat') {
      return this.handleChat(request);
    }
    
    return new Response('Not found', { status: 404 });
  }

  // Called when agent starts
  async onStart() {
    console.log('Agent started');
  }

  // Called when agent stops
  async onStop() {
    console.log('Agent stopped');
  }
}

State Management

Agents can maintain state across requests:

import { Agent } from 'agents';

export class CounterAgent extends Agent {
  private count = 0;

  async onRequest(request: Request) {
    if (request.method === 'POST') {
      this.count++;
    }
    
    return Response.json({ count: this.count });
  }
}

WebSocket Support

Agents can handle WebSocket connections:

import { Agent } from 'agents';

export class RealtimeAgent extends Agent {
  async onRequest(request: Request) {
    const upgradeHeader = request.headers.get('Upgrade');
    
    if (upgradeHeader === 'websocket') {
      return this.handleWebSocket(request);
    }
    
    return new Response('Expected WebSocket', { status: 400 });
  }

  async handleWebSocket(request: Request) {
    const pair = new WebSocketPair();
    const [client, server] = Object.values(pair);

    server.accept();
    server.addEventListener('message', (event) => {
      server.send(`Echo: ${event.data}`);
    });

    return new Response(null, {
      status: 101,
      webSocket: client,
    });
  }
}

AI Integration

Agents work well with Cloudflare AI:

import { Agent } from 'agents';

export class AssistantAgent extends Agent {
  async onRequest(request: Request) {
    const { message } = await request.json();
    
    const response = await this.env.AI.run('@cf/meta/llama-2-7b-chat-int8', {
      messages: [
        { role: 'system', content: 'You are a helpful assistant.' },
        { role: 'user', content: message },
      ],
    });
    
    return Response.json({ response: response.response });
  }
}

Routing to Agents

Use middleware to route requests to agents:

// src/middleware.ts
import { defineMiddleware } from 'koze:middleware';

export default defineMiddleware({
  agents: {
    async request(ctx, next) {
      if (!ctx.url.pathname.startsWith('/agents/')) {
        return next();
      }

      const agentName = ctx.url.pathname.split('/')[2];
      const stub = ctx.env[`${agentName.toUpperCase()}_AGENT`];
      
      if (!stub) {
        return new Response('Agent not found', { status: 404 });
      }

      return stub.fetch(ctx.request);
    },
  },
});

Wrangler Configuration

Agent files are re-exported from src/worker.ts (via the koze:worker virtual module), but Wrangler needs matching bindings:

{
  "durable_objects": {
    "bindings": [
      {
        "name": "SESSION_AGENT",
        "class_name": "SessionAgent"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_classes": ["SessionAgent"]
    }
  ]
}