Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions .agents/skills/build-agent/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
name: build-agent
description: Build and ship a new installable agent in this repo, following the web-agent pattern from development to CLI to production. USE WHEN adding a new agent, adding tools to an agent, exposing an agent through the agentcn CLI/registry, writing agent docs, or wiring the docs demo. Trigger words - new agent, add agent, add a tool, register agent, agent registry, agentcn add, agent docs demo.
---

# Build an Agent (dev → CLI → prod)

This skill is the canonical, repeatable recipe for adding a new agent to this
repo. It mirrors the reference implementation, the **web-agent**
(`ai/agents/web/`), which is distributed to users through the `agentcn` CLI.

Follow the checklist top-to-bottom. Each phase links to a deeper reference file
under `references/`. Always copy the existing web-agent conventions instead of
inventing new ones — consistency is what makes the registry + CLI work.

## Mental model

An agent in this repo has **five layers**. Adding an agent means touching each:

1. **Source** — the agent + tools live in `ai/agents/<name>/`. This is the code
users actually run. (→ `references/agent-anatomy.md`)
2. **Registry** — `registry/registry-agents.ts` declares which files, deps, and
env vars make up the agent. A build script turns this into static JSON at
`apps/web/public/r/<name>.json`. (→ `references/registry-cli-prod.md`)
3. **CLI** — `agentcn add <name>` fetches that JSON and installs the agent into a
user's project. The CLI is generic; you don't edit it per-agent. (→
`references/registry-cli-prod.md`)
4. **Docs** — `apps/web/content/docs/agents/<name>.mdx` documents install +
wiring. (→ `references/docs-and-demo.md`)
5. **Demo** — `apps/web/lib/agent-demos/<name>.ts` powers the zero-cost
simulated preview shown in the docs. (→ `references/docs-and-demo.md`)

## Conventions (do not deviate)

- **Package manager:** `pnpm` (see `packageManager` in root `package.json`).
- **Task runner:** Nx. Prefer `pnpm exec nx run ...` or the root `package.json`
scripts over calling tools directly.
- **Model:** `anthropic("claude-sonnet-4-5-20250929")` via `@ai-sdk/anthropic`.
- **AI SDK:** Vercel `ai` (`streamText`, `tool`, `stepCountIs`).
- **Tool schemas:** `zod`, defined in a dedicated `schema.ts`, imported by tools.
- **Agent name:** kebab-case, matches the folder, the registry `name`, the docs
slug, and the demo `agentId` — all identical. E.g. `web-agent`.
- **Env vars:** never hardcode secrets. Read from `process.env` and throw a clear
error if missing (see `tools/core.ts`).

## Checklist

### Phase 1 — Scaffold the source (`ai/agents/<name>/`)

Copy the web-agent layout. Full templates in `references/agent-anatomy.md`.

- [ ] `ai/agents/<name>/index.ts` — re-export the agent: `export { <name>Agent } from "./agent"`.
- [ ] `ai/agents/<name>/agent.ts` — `streamText` call with model, system prompt, tools, `stopWhen`.
- [ ] `ai/agents/<name>/prompt.ts` — the `SYSTEM_PROMPT` string.
- [ ] `ai/agents/<name>/tools/schema.ts` — one zod schema per tool.
- [ ] `ai/agents/<name>/tools/core.ts` — shared clients/helpers + env-var guards.
- [ ] `ai/agents/<name>/tools/<tool>.ts` — one file per tool using `tool({...})`.
- [ ] `ai/agents/<name>/tools/toolset.ts` — map tool names → tool defs.
- [ ] `ai/agents/<name>/tools/index.ts` — `export { <name>Toolset } from "./toolset"`.
- [ ] `ai/agents/<name>/tools/types.ts` — shared TS types (optional).
- [ ] `ai/agents/<name>/tools/services/*.ts` — external API clients (optional).

### Phase 2 — Tests (`ai/agents/<name>/test/`)

- [ ] `test/test-helpers.ts` — `describeIf<Provider>` guards keyed on env vars.
- [ ] `test/<tool>.test.ts` — one suite per tool; live-API suites use the guards.
- [ ] Run `pnpm test:web-agent`-equivalent: `pnpm jest ai/agents/<name>`.

### Phase 3 — Register for distribution

- [ ] Add an entry to the `agents` array in `registry/registry-agents.ts`
(name, description, title, categories, `dependencies`, `envVars`, and every
file from Phase 1 with its `type`).
- [ ] Build the registry: `pnpm agentcn:registry:build`.
- [ ] Confirm `apps/web/public/r/<name>.json` and updated `index.json` exist.

### Phase 4 — Docs + demo

- [ ] `apps/web/content/docs/agents/<name>.mdx` — frontmatter (`title`,
`description`, `component: true`), `<AgentDemoPreview agentId="<name>" />`,
install tabs, wiring, tools reference.
- [ ] Add the slug to `pages` in `apps/web/content/docs/agents/meta.json`.
- [ ] `apps/web/lib/agent-demos/<name>.ts` — an `AgentDemoConfig` with scenarios.
- [ ] Register it in `apps/web/lib/agent-demos/index.ts` (`agentDemos` map).

### Phase 5 — Verify (dev) and ship (prod)

- [ ] `pnpm exec nx run @kit/web:typecheck`
- [ ] `pnpm exec nx run @kit/web:build`
- [ ] `pnpm deploy:build` (registry build + web build — what prod runs).
- [ ] Optional live check: `pnpm agentcn:registry:verify-live`.
- [ ] Ship: registry JSON deploys with the web app; the CLI is published via
`nx release` on an `agentcn@*` tag. See `references/registry-cli-prod.md`.

## Quick command reference

```bash
pnpm jest ai/agents/<name> # run agent tests
pnpm agentcn:registry:build # regenerate public/r/*.json (REQUIRED after registry edits)
pnpm exec nx run @kit/web:typecheck # typecheck the web app + agent source
pnpm exec nx run @kit/web:build # build docs/marketing site
pnpm deploy:build # registry build + web build (prod parity)
npx agentcn@latest add <name> # what a user runs to install your agent
```

## Common mistakes

- **Forgetting `pnpm agentcn:registry:build`** after editing source or the
registry — the CLI serves stale JSON and installs the old files.
- **Name drift** — folder, registry `name`, docs slug, and demo `agentId` must be
identical.
- **Adding a file to `ai/agents/<name>/` but not to `files` in the registry** —
the CLI won't install it, so the agent breaks in the user's project.
- **New dependency not listed in the registry `dependencies`** — user install
fails at runtime. Keep `dependencies`/`envVars` in sync with the source.
202 changes: 202 additions & 0 deletions .agents/skills/build-agent/references/agent-anatomy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# Agent anatomy — source layout & templates

Reference implementation: `ai/agents/web/`. Copy this structure verbatim for a
new agent, replacing `web`/`<name>` and the tool set.

## Directory layout

```
ai/agents/<name>/
├── index.ts # public entry: re-exports the agent
├── agent.ts # the agent (streamText call)
├── prompt.ts # SYSTEM_PROMPT string
├── tools/
│ ├── index.ts # re-exports the toolset
│ ├── toolset.ts # { tool_name: toolDef } passed to the agent
│ ├── schema.ts # zod input schemas (one per tool)
│ ├── core.ts # shared clients + env-var guards + helpers
│ ├── types.ts # shared TS types (optional)
│ ├── <tool>.ts # one file per tool (tool({...}))
│ └── services/
│ └── <service>.ts # external API client wrappers (optional)
└── test/
├── test-helpers.ts # describeIf<Provider> guards
└── <tool>.test.ts # one suite per tool
```

## Layer 1 — `index.ts`

Thin public surface. Consumers import from `ai/agents/<name>`.

```ts
export { webAgent } from "./agent";
```

## Layer 2 — `agent.ts`

The agent is a plain function that returns a `streamText` result. It wires the
model, system prompt, tools, and a stop condition. Keep it this small — all the
behavior lives in the prompt and tools.

```ts
import { anthropic } from "@ai-sdk/anthropic";
import { streamText, type ModelMessage, stepCountIs } from "ai";
import { SYSTEM_PROMPT } from "./prompt";
import { webToolset } from "./tools";

export function webAgent(messages: ModelMessage[]) {
return streamText({
model: anthropic("claude-sonnet-4-5-20250929"),
system: SYSTEM_PROMPT,
messages,
tools: webToolset,
stopWhen: [stepCountIs(20)],
});
}
```

Notes:
- **`messages: ModelMessage[]`** in, streaming result out. The caller (an API
route, script, or the demo app) owns the transport.
- **`stopWhen: [stepCountIs(20)]`** caps the tool-use loop. Tune per agent.
- Keep the model choice consistent across agents unless there's a reason.

## Layer 3 — `prompt.ts`

Export a single `SYSTEM_PROMPT` constant. Describe the agent's role, when to use
each tool, and output/citation rules. Keep tool names in the prompt exactly
matching the `toolset.ts` keys.

```ts
export const SYSTEM_PROMPT = `You are a web research agent.
...
Use web_search for quick lookups. Use deep_research for multi-source reports.
Always cite sources as markdown links.`;
```

## Layer 4 — Tools

### `tools/schema.ts` — validation

One zod schema per tool. Use `.describe()` on every field — the model reads
these descriptions.

```ts
import { z } from "zod";

export const webSearchSchema = z.object({
query: z.string().describe("The search query."),
num_results: z
.number()
.int()
.min(1)
.max(25)
.optional()
.default(5)
.describe("Maximum number of search results."),
});
```

### `tools/core.ts` — shared clients & env guards

Centralize provider clients and secret access. Every secret read throws a clear
error when missing (never silently fall back).

```ts
import Exa from "exa-js";

export function getExaClient() {
const exaApiKey = process.env["EXA_API_KEY"];
if (!exaApiKey) {
throw new Error("EXA_API_KEY is not set.");
}
return new Exa(exaApiKey);
}
```

Put filesystem/artifact helpers here too if the agent persists output (the
web-agent writes to `data/<name>.local/` via `saveArtifact`).

### `tools/<tool>.ts` — a tool

Each tool is `tool({ description, inputSchema, execute })`. `execute` receives
the validated, typed args. Return a serializable object; a `success` flag plus a
`content` payload is the house style.

```ts
import { tool } from "ai";
import { getExaClient } from "./core";
import { webSearchSchema } from "./schema";

export const webSearchTool = tool({
description: "Search the web for up-to-date information.",
inputSchema: webSearchSchema,
execute: async ({ query, num_results }) => {
const exa = getExaClient();
const { results } = await exa.searchAndContents(query, {
numResults: num_results,
highlights: true,
});
return {
success: true,
content: results.map((r) => ({
title: r.title,
url: r.url,
content: r.highlights.join("\n"),
})),
};
},
});
```

### `tools/toolset.ts` — the map

Keys are the tool names the model calls (and what you name in the prompt).

```ts
import { ToolSet } from "ai";
import { webSearchTool } from "./web-search";
import { answerQuestionTool } from "./answer-question";

export const webToolset = {
web_search: webSearchTool,
answer_question: answerQuestionTool,
} as ToolSet;
```

### `tools/index.ts`

```ts
export { webToolset } from "./toolset";
```

## Layer 5 — Tests

Live-API tests are gated so CI/local runs without keys don't fail.

`test/test-helpers.ts`:

```ts
import { describe } from "@jest/globals";
declare const process: { env: Record<string, string | undefined> };

export const describeIfExa =
process.env.EXA_API_KEY ? describe : describe.skip;
```

`test/web-search.test.ts`:

```ts
import { describeIfExa } from "./test-helpers";

describeIfExa("web_search (live)", () => {
it("returns results", async () => {
// ... call the tool's execute and assert on shape
});
});
```

Run: `pnpm jest ai/agents/<name>`.

> Tests that hit a real provider will fail without network + keys — that's
> expected. Gate them behind `describeIf<Provider>` so they skip cleanly.
Loading
Loading