Getting Started

Capstan is a Bun-native AI agent framework. This guide walks you through building an agent first (the primary use case), then covers full-stack web apps.

Prerequisites

  • Bun 1.1+ or Node.js 20+ (ES2022, ESM-only)
  • An LLM API key (OpenAI, Anthropic, or any OpenAI-compatible endpoint)

Installation

# Bun
bun add @zauso-ai/capstan-ai @zauso-ai/capstan-agent

# npm
npm install @zauso-ai/capstan-ai @zauso-ai/capstan-agent

Path 1: Build an AI Agent

Step 1: Your first agent

import { createSmartAgent } from "@zauso-ai/capstan-ai";
import { openaiProvider } from "@zauso-ai/capstan-agent";

const agent = createSmartAgent({
  llm: openaiProvider({ apiKey: process.env.OPENAI_API_KEY! }),
  tools: [],
});

const result = await agent.run("Explain TypeScript in one sentence");
console.log(result.result);

With no tools the agent is a simple chat wrapper. run() returns an AgentRunResult with:

  • result -- the agent's final output
  • status -- "completed", "max_iterations", "approval_required", "paused", "canceled", or "fatal"
  • iterations -- number of LLM round-trips
  • toolCalls -- array of every tool invocation

To use Anthropic instead:

import { anthropicProvider } from "@zauso-ai/capstan-agent";

const agent = createSmartAgent({
  llm: anthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! }),
  tools: [],
});

Both providers accept an optional model and baseUrl override:

openaiProvider({
  apiKey: process.env.OPENAI_API_KEY!,
  model: "gpt-4o",
  baseUrl: "https://api.openai.com/v1",
});

anthropicProvider({
  apiKey: process.env.ANTHROPIC_API_KEY!,
  model: "claude-sonnet-4-20250514",
});

Step 2: Add tools

Tools are operations with defined inputs and outputs. The agent decides when to call them.

import { createSmartAgent } from "@zauso-ai/capstan-ai";
import type { AgentTool } from "@zauso-ai/capstan-ai";
import { openaiProvider } from "@zauso-ai/capstan-agent";
import { readFileSync, writeFileSync } from "node:fs";
import { execSync } from "node:child_process";

const readFile: AgentTool = {
  name: "read_file",
  description: "Read the contents of a file at the given path",
  parameters: {
    type: "object",
    properties: {
      path: { type: "string", description: "Absolute file path" },
    },
    required: ["path"],
  },
  async execute(args) {
    return readFileSync(args.path as string, "utf-8");
  },
};

const writeFile: AgentTool = {
  name: "write_file",
  description: "Write content to a file, creating or overwriting it",
  parameters: {
    type: "object",
    properties: {
      path: { type: "string", description: "Absolute file path" },
      content: { type: "string", description: "File content to write" },
    },
    required: ["path", "content"],
  },
  async execute(args) {
    writeFileSync(args.path as string, args.content as string);
    return "File written successfully";
  },
};

const runCommand: AgentTool = {
  name: "run_command",
  description: "Execute a shell command and return its output",
  parameters: {
    type: "object",
    properties: {
      command: { type: "string", description: "Shell command to run" },
    },
    required: ["command"],
  },
  timeout: 30_000,
  async execute(args) {
    return execSync(args.command as string, { encoding: "utf-8" });
  },
};

const agent = createSmartAgent({
  llm: openaiProvider({ apiKey: process.env.OPENAI_API_KEY! }),
  tools: [readFile, writeFile, runCommand],
  maxIterations: 20,
});

const result = await agent.run("Read package.json and list the dependencies");
console.log(result.result);

Every tool has:

FieldTypePurpose
namestringUnique identifier the agent uses to call the tool
descriptionstringTells the agent what the tool does
parametersJSON SchemaDefines the input shape; auto-validated before execute
execute(args) => Promise<unknown>The function that runs when the agent calls the tool
validate(args) => { valid, error? }Optional argument validator, runs before execute
timeoutnumberOptional max execution time in milliseconds
failureMode"soft" | "hard""soft" returns error to agent; "hard" (default) aborts
isConcurrencySafebooleanWhether the tool can run concurrently with others

Step 3: Add skills

Skills are strategies -- high-level guidance for how to approach a class of problems. They differ from tools: tools are operations (read a file, run a test), while skills are playbooks (how to debug a failing test).

import { createSmartAgent, defineSkill } from "@zauso-ai/capstan-ai";
import { openaiProvider } from "@zauso-ai/capstan-agent";

const tddDebug = defineSkill({
  name: "tdd-debug",
  description: "Test-driven debugging workflow",
  trigger: "when tests fail or a bug needs fixing",
  prompt: [
    "1. Read the failing test to understand the expected behavior",
    "2. Read the source code under test",
    "3. Identify the root cause",
    "4. Write the fix",
    "5. Re-run tests to confirm the fix",
    "6. If tests still fail, repeat from step 1",
  ].join("\n"),
  tools: ["read_file", "write_file", "run_command"],
});

const agent = createSmartAgent({
  llm: openaiProvider({ apiKey: process.env.OPENAI_API_KEY! }),
  tools: [readFile, writeFile, runCommand],
  skills: [tddDebug],
});

When skills are configured, the agent automatically gets an activate_skill meta-tool. During a run, the agent can call activate_skill({ skill_name: "tdd-debug" }) to receive the skill's guidance prompt injected into the conversation.

Step 4: Add self-evolution

Evolution makes the agent learn from experience. After each run, the agent's trajectory is captured, distilled into strategies, and applied to future runs.

import { createSmartAgent } from "@zauso-ai/capstan-ai";
import { SqliteEvolutionStore } from "@zauso-ai/capstan-ai/evolution";
import { openaiProvider } from "@zauso-ai/capstan-agent";

const agent = createSmartAgent({
  llm: openaiProvider({ apiKey: process.env.OPENAI_API_KEY! }),
  tools: [readFile, writeFile, runCommand],
  skills: [tddDebug],
  evolution: {
    store: new SqliteEvolutionStore("./agent-evolution.db"),
    capture: "every-run",
    distillation: "post-run",
    pruning: { maxStrategies: 50, minUtility: 0.2 },
    skillPromotion: { minUtility: 0.7, minApplications: 5 },
  },
});

What happens across multiple runs:

  1. Run 1-5: The agent solves tasks. Each run's trajectory (tool calls, outcomes) is captured as an experience.
  2. Run 5-20: Post-run distillation extracts strategies from experiences. The agent queries relevant strategies before starting new tasks.
  3. Run 20+: High-performing strategies that exceed the utility and application thresholds are promoted to reusable skills automatically.

Step 5: Production hardening

Add resilience, cost controls, and timeout protection for production deployments.

const agent = createSmartAgent({
  llm: openaiProvider({ apiKey: process.env.OPENAI_API_KEY!, model: "gpt-4o" }),
  fallbackLlm: anthropicProvider({ apiKey: process.env.ANTHROPIC_API_KEY! }),
  tools: [readFile, writeFile, runCommand],
  maxIterations: 30,
  tokenBudget: { maxOutputTokensPerTurn: 16_000, nudgeAtPercent: 80 },
  toolResultBudget: {
    maxChars: 50_000,
    persistDir: "./tool-results",
    maxAggregateCharsPerIteration: 200_000,
  },
  llmTimeout: {
    chatTimeoutMs: 120_000,
    streamIdleTimeoutMs: 90_000,
    stallWarningMs: 30_000,
  },
  hooks: {
    beforeToolCall: async (tool, args) => {
      console.log(`Calling ${tool}`, args);
      return { allowed: true };
    },
    afterToolCall: async (tool, args, result, status) => {
      console.log(`${tool} ${status}`);
    },
    onRunComplete: async (result) => {
      console.log(`Run finished: ${result.status}, ${result.iterations} iterations`);
    },
  },
});
OptionPurpose
fallbackLlmAutomatic failover when the primary LLM returns errors
maxIterationsHard cap on LLM round-trips to prevent runaway loops
tokenBudgetControls output token spend per turn; nudgeAtPercent tells the agent to finish up
toolResultBudgetTruncates oversized tool results; persistDir writes full results to disk for debugging
llmTimeoutTimeouts for chat calls, stream idle gaps, and stall warnings
hooksLifecycle callbacks for logging, gating, and monitoring

Step 6: Testing your agent

Create a .env.test file with LLM credentials, then use describeWithLLM to run tests once per configured provider:

import { it, expect } from "bun:test";
import { createSmartAgent } from "@zauso-ai/capstan-ai";
import { describeWithLLM } from "./helpers/env.js";

describeWithLLM("My agent", (provider) => {
  it("can read a file", async () => {
    const agent = createSmartAgent({
      llm: provider,
      tools: [readFile],
      maxIterations: 10,
    });

    const result = await agent.run("Read package.json and tell me the project name");

    expect(result.status).toBe("completed");
    expect(result.toolCalls.length).toBeGreaterThanOrEqual(1);
    expect(result.toolCalls[0].tool).toBe("read_file");
  }, 120_000);
});

Tests using describeWithLLM are automatically skipped in CI when LLM_API_KEY is not set, so they never break your pipeline.

# Run all LLM tests
bun test tests/e2e/llm/

# Run a specific test file
bun test tests/e2e/llm/smoke.test.ts

Path 2: Build a Full-Stack Web App

Capstan also drives HTTP APIs, server-rendered React pages, database models, and multi-protocol agent endpoints from a single codebase.

Create a project

bunx create-capstan-app@beta my-app
cd my-app
bun install
bunx capstan dev

The scaffolder supports two templates:

TemplateDescription
blankMinimal project with a health check API and home page
ticketsFull example with a Ticket model, CRUD API routes, and auth policy

Project structure

my-app/
  app/
    routes/
      _layout.tsx          # Root layout (wraps all pages)
      index.page.tsx       # Home page
      api/
        health.api.ts      # Health check endpoint
    models/                # Data model definitions
    styles/
      main.css             # CSS entry point
    migrations/            # Database migration files
    policies/
      index.ts             # Permission policies
  capstan.config.ts        # Framework configuration
  package.json
  tsconfig.json
  AGENTS.md                # AI coding agent guide

Add an API

Create app/routes/api/greet.api.ts:

import { defineAPI } from "@zauso-ai/capstan-core";
import { z } from "zod";

export const GET = defineAPI({
  input: z.object({ name: z.string().optional() }),
  output: z.object({ message: z.string() }),
  description: "Greet a user by name",
  capability: "read",
  async handler({ input }) {
    return { message: `Hello, ${input.name ?? "world"}!` };
  },
});

defineAPI() provides input/output validation via Zod schemas and multi-protocol projection -- the same definition drives MCP tools, A2A skills, and OpenAPI specs.

Auto-generated agent endpoints

Once the dev server is running, these endpoints are available automatically:

EndpointPurpose
GET /.well-known/capstan.jsonCapstan agent manifest
GET /.well-known/agent.jsonA2A agent card
POST /.well-known/a2aA2A JSON-RPC handler
GET /openapi.jsonOpenAPI 3.1.0 spec
bunx capstan mcpMCP server over stdio

Run

# Development
bunx capstan dev

# Production build
bunx capstan build

# Start production server
bunx capstan start

Next Steps

  • Core Concepts -- createSmartAgent, agent loop, defineAPI, multi-protocol
  • API Reference -- full API surfaces across all Capstan packages
  • Database -- defineModel, CRUD helpers, migrations, vector search
  • Authentication -- JWT sessions, API keys, OAuth
  • Deployment -- production build and deployment
Tip: Use <Link> from @zauso-ai/capstan-react/client instead of plain <a> tags for client-side navigation with automatic prefetching and SPA transitions.