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-agentPath 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 outputstatus--"completed","max_iterations","approval_required","paused","canceled", or"fatal"iterations-- number of LLM round-tripstoolCalls-- 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:
| Field | Type | Purpose |
|---|---|---|
name | string | Unique identifier the agent uses to call the tool |
description | string | Tells the agent what the tool does |
parameters | JSON Schema | Defines 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 |
timeout | number | Optional max execution time in milliseconds |
failureMode | "soft" | "hard" | "soft" returns error to agent; "hard" (default) aborts |
isConcurrencySafe | boolean | Whether 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:
- Run 1-5: The agent solves tasks. Each run's trajectory (tool calls, outcomes) is captured as an experience.
- Run 5-20: Post-run distillation extracts strategies from experiences. The agent queries relevant strategies before starting new tasks.
- 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`);
},
},
});| Option | Purpose |
|---|---|
fallbackLlm | Automatic failover when the primary LLM returns errors |
maxIterations | Hard cap on LLM round-trips to prevent runaway loops |
tokenBudget | Controls output token spend per turn; nudgeAtPercent tells the agent to finish up |
toolResultBudget | Truncates oversized tool results; persistDir writes full results to disk for debugging |
llmTimeout | Timeouts for chat calls, stream idle gaps, and stall warnings |
hooks | Lifecycle 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.tsPath 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 devThe scaffolder supports two templates:
| Template | Description |
|---|---|
blank | Minimal project with a health check API and home page |
tickets | Full 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 guideAdd 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:
| Endpoint | Purpose |
|---|---|
GET /.well-known/capstan.json | Capstan agent manifest |
GET /.well-known/agent.json | A2A agent card |
POST /.well-known/a2a | A2A JSON-RPC handler |
GET /openapi.json | OpenAPI 3.1.0 spec |
bunx capstan mcp | MCP server over stdio |
Run
# Development
bunx capstan dev
# Production build
bunx capstan build
# Start production server
bunx capstan startNext 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
<Link> from @zauso-ai/capstan-react/client instead of plain <a> tags for client-side navigation with automatic prefetching and SPA transitions.