From rsturla-skills
Build TypeScript AI agents with eval framework, provider abstraction, and production guardrails. Uses Vercel AI SDK for model-agnostic tool-use loops, Zod for structured outputs, Vitest for evals, and OpenTelemetry for observability. Covers prompt caching, extended thinking, RAG pipelines, structured output enforcement, tool annotations, seed tool calls, toolcall regression evals, and duplicate call detection. Use when the user wants to build an agent, create an AI assistant, implement tool-use, or scaffold an agentic system — even if they just say "build a bot", "add AI to this", or "make it agentic."
How this skill is triggered — by the user, by Claude, or both
Slash command
/rsturla-skills:build-agentThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Build TypeScript AI agents that are provider-agnostic, eval-tested, and production-ready.
Build TypeScript AI agents that are provider-agnostic, eval-tested, and production-ready.
User: /build-agent
Agent: asks agent purpose, tools needed, provider preference
Agent: scaffolds project with agent loop, tools, evals, observability
User: reviews, iterates
Follow Anthropic's guidance: start simple, add complexity only when needed.
| Level | Pattern | When |
|---|---|---|
| 1 | Single LLM call | Straightforward tasks with no tool use |
| 2 | Prompt chain | Fixed sequential steps with validation gates |
| 3 | Router | Distinct input categories need different handling |
| 4 | Parallelization | Independent subtasks or voting |
| 5 | Agent loop | Open-ended problems, unknown number of steps |
| 6 | Multi-agent | Specialized capabilities need delegation |
Default to level 5 (agent loop) unless the user specifies otherwise. Most "build an agent" requests need a tool-use loop.
Ask in order:
<agent-name>/
├── src/
│ ├── index.ts # Entry point
│ ├── agent.ts # Agent loop + configuration
│ ├── provider.ts # Model provider setup
│ ├── cost.ts # Token pricing + cost calculation
│ ├── feedback.ts # Feedback collection + storage
│ ├── experiment.ts # A/B testing framework
│ ├── tools/
│ │ ├── index.ts # Tool registry (re-exports all tools)
│ │ └── <tool-name>.ts # One file per tool
│ └── types.ts # Shared types and Zod schemas
├── evals/
│ ├── setup.ts # Eval helpers and fixtures
│ ├── scorers.ts # Custom scoring functions
│ └── <scenario>.eval.ts # One file per eval scenario
├── scripts/
│ ├── feedback-report.ts # Feedback summary CLI
│ ├── feedback-to-evals.ts # Mine corrections into eval cases
│ └── run-experiment.ts # Offline A/B experiment runner
├── data/ # Gitignored
│ ├── feedback.jsonl # Collected feedback
│ └── experiments/ # Experiment results
├── package.json
├── tsconfig.json
├── vitest.config.ts # Separate config for evals
└── biome.json
Initialize with:
bun init
# Remove bun init artifacts (index.ts, README.md) — project uses src/ structure
rm -f index.ts README.md
bun add ai @ai-sdk/google-vertex zod
bun add -d vitest @biomejs/biome
Add provider packages only for providers the user needs. Always install ai (Vercel AI SDK core).
System prompt defines agent behavior. Write it before code — the prompt IS the spec.
[Role] — who the agent is, one sentence
[Task] — what it does, scope boundaries
[Rules] — hard constraints (always/never)
[Output format] — how to structure responses
[Examples] — 2-3 input/output pairs showing ideal behavior
const SYSTEM_PROMPT = `You are a weather assistant. You provide current conditions and forecasts for cities worldwide.
Rules:
- Always include temperature, humidity, and wind speed
- Use metric units (Celsius, km/h) unless the user requests imperial
- If a city is not found, say so clearly — do not guess
- Do not answer questions unrelated to weather
When the user asks about weather, call the weather tool with the city name. If the user mentions multiple cities,
call the tool once per city.
Examples:
User: "What's the weather in London?"
→ Call weather tool with city="London"
→ Respond with current conditions including temperature, humidity, wind
User: "Compare Tokyo and Sydney"
→ Call weather tool with city="Tokyo"
→ Call weather tool with city="Sydney"
→ Respond with side-by-side comparison`;
Never tune prompts without evals. You'll fix one case and break three others.
Vercel AI SDK handles provider abstraction. src/provider.ts is the only file that changes when switching
providers:
// src/provider.ts
import { vertex } from "@ai-sdk/google-vertex";
export const MODEL_ID = "claude-sonnet-4-6";
export function createModel() { return vertex(MODEL_ID); }
Swap provider by changing import and model ID. See REFERENCE.md for all provider examples.
One file per tool, Zod schema for inputs. Key rules:
.min(), .max(), .int().describe() on each field — model reads these{ error: "..." }, never throwsrc/tools/index.tsSee REFERENCE.md for full tool example and reusable wrapper.
Mandatory. src/cost.ts maps model IDs to per-token pricing. Every AgentResult includes usage.costUsd.
No silent token consumption — if model pricing is unknown, log a warning.
See REFERENCE.md for full calculateCost implementation and pricing table.
// src/agent.ts
import { generateText, stepCountIs, type StepResult } from "ai";
import type { ModelMessage } from "ai";
import { createModel, MODEL_ID } from "./provider.js";
import { calculateCost, type UsageRecord } from "./cost.js";
import * as tools from "./tools/index.js";
export interface AgentConfig {
system: string;
maxSteps: number;
variant?: string;
}
export interface AgentResult {
runId: string;
sessionId: string;
text: string;
stepCount: number;
steps: StepResult<typeof tools>[];
usage: UsageRecord;
durationMs: number;
variant: string;
}
export async function runAgent(
prompt: string,
config: AgentConfig,
sessionId?: string,
): Promise<AgentResult> {
const messages: ModelMessage[] = [{ role: "user", content: prompt }];
const startTime = Date.now();
const result = await generateText({
model: createModel(),
system: config.system,
messages,
tools,
stopWhen: stepCountIs(config.maxSteps),
});
const usage = calculateCost(
MODEL_ID,
result.totalUsage.inputTokens ?? 0,
result.totalUsage.outputTokens ?? 0,
);
return {
runId: crypto.randomUUID(),
sessionId: sessionId ?? crypto.randomUUID(),
text: result.text,
stepCount: result.steps.length,
steps: result.steps,
usage,
durationMs: Date.now() - startTime,
variant: config.variant ?? "default",
};
}
AI SDK stopWhen handles the loop. Use manual loop from REFERENCE.md only for custom per-step logic (cost
budgets, loop detection, conditional tool filtering).
| Control | Default | Purpose |
|---|---|---|
stopWhen | stepCountIs(25) | Hard cap on iterations |
| Wall-clock timeout | 300s | Prevent runaway agents |
| Cost budget | $2.00/run | Abort if usage.costUsd exceeds limit |
| Loop detection | Fingerprint last 3 tool calls | Catch repetitive behavior |
Evals are not optional. Every agent ships with evals. Every eval records cost.
Three layers, cheapest first:
| Layer | Approach | Cost | Use For |
|---|---|---|---|
| Deterministic | Regex, exact match, JSON schema | Free | Format compliance, required fields |
| Semantic | Embedding distance | Low | Meaning preservation |
| LLM-as-judge | Model grades model output | Medium | Tone, helpfulness, correctness |
// vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
include: ["evals/**/*.eval.ts"],
testTimeout: 120_000,
maxConcurrency: 3,
},
});
Every eval calls recordEval() with score, tokens, cost, and duration. afterAll prints a cost report and
checks against EVAL_COST_CEILING_USD. Example:
// evals/weather.eval.ts
import { describe, it, expect } from "vitest";
import { runAgent } from "../src/agent.js";
import { containsAll } from "./scorers.js";
import { recordEval } from "./setup.js";
describe("weather agent", () => {
it("returns temperature for valid city", async () => {
const result = await runAgent("What is the weather in London?",
{ system: "You are a helpful weather assistant.", maxSteps: 10 },
);
const score = containsAll(["london", "temperature"])(result.text);
recordEval({
name: "weather/valid-city",
score: score.score,
tokens: result.usage.totalTokens,
costUsd: result.usage.costUsd,
durationMs: result.durationMs,
});
expect(score.score).toBe(1);
});
});
Run with bun vitest run --config vitest.config.ts. Set cost ceiling for suites (e.g., $1.00/run).
See REFERENCE-EVALS.md for scorer implementations (deterministic, LLM-as-judge), eval cost tracking setup, trajectory evaluation, and regression detection.
Evals measure offline quality. Production feedback measures real-world quality. A/B tests compare alternatives. All three feed the iteration loop.
Collect structured feedback after deployment. Every AgentResult includes runId and sessionId — clients
submit feedback asynchronously (seconds to hours after response).
Two signal types:
| Type | Source | Signal |
|---|---|---|
| Explicit | Thumbs up/down, written correction | Direct quality signal (high precision) |
| Implicit | Retries, abandonment, time-to-next-action | Indirect signal (high volume) |
Storage is pluggable — FeedbackStore interface with JSONL backend for dev, database for production. Expose
feedback submission via HTTP endpoint so any client can report.
Corrections are the highest-value signal — negative feedback with a correction becomes a future eval case.
Run bun scripts/feedback-to-evals.ts to mine corrections into evals/fixtures/from-feedback.json.
See REFERENCE-FEEDBACK.md for full FeedbackSchema, storage backends, API endpoint, implicit signal detection, feedback summary/reporting, and feedback-to-eval pipeline.
Two modes:
bun scripts/run-experiment.ts
See REFERENCE-FEEDBACK.md for full experiment runner (offline + online), variant assignment, cost-quality tradeoff analysis, and graduation criteria.
1. Write evals → establish baseline scores
2. Deploy agent → collect feedback
3. Mine negative feedback → new eval cases
4. A/B test changes → compare against baseline
5. Ship winner → update baseline
6. Repeat
Use OpenTelemetry. The Vercel AI SDK has built-in telemetry support:
// src/agent.ts — add to generateText call
const result = await generateText({
model: createModel(),
system: config.system,
messages,
tools,
stopWhen: stepCountIs(config.maxSteps),
experimental_telemetry: {
isEnabled: true,
metadata: { agentName: "weather-agent" },
},
});
For custom spans around tool execution, see REFERENCE.md.
Present the complete file tree and ask user to review before any further work. Suggest:
bun vitest runbun scripts/run-experiment.ts/scaffold-repobun. Never npm install, npx, yarn, or pnpmsrc/provider.ts. Do NOT build custom abstraction layers on top of the
AI SDK. The SDK already handles thisstopWhen vs manual loop — use stopWhen: stepCountIs(n) (AI SDK built-in) by default. Only write a
manual while-loop when you need custom per-step logic (cost tracking, conditional tool filtering, step-level
logging). See REFERENCE.md for the manual loop pattern{ error: "message" } so the model can recovertestTimeout: 120_000 minimum. Use pool: "threads" with
maxThreads: 3 for parallel execution without overwhelming rate limits.describe() matters — the model sees field descriptions when deciding how to call tools. Missing
descriptions = worse tool use@ai-sdk/google-vertex package and set
GOOGLE_VERTEX_PROJECT and GOOGLE_VERTEX_LOCATION env vars. Model IDs: claude-sonnet-4-6,
claude-opus-4-7, claude-haiku-4-5 (no date suffixes)AgentResult must include usage.costUsd. Unknown models throw, not
silently report $0.00. Add model to PRICING table before usewithRetry() from REFERENCE.md. Never let
a single API failure crash the agentdefineValidatedTool in REFERENCE.mdstreamText for user-facing agents, generateText for batch/evals. See
REFERENCE.md streaming sectiondata/ but back it
up. Never delete feedback without extracting insights firstSee also:
npx claudepluginhub rsturla/skillsGuides collaborative design exploration before implementation: explores context, asks clarifying questions, proposes approaches, and writes a design doc for user approval.
Creates structured, bite-sized implementation plans from specs or requirements before writing code. Useful for breaking down multi-step tasks into testable steps with file structure and task boundaries.
Synthesizes the current conversation into a structured spec (PRD) and publishes it to the project issue tracker with a ready-for-agent label, without interviewing the user.