Skip to content
Back to skills

Create Agent

ASecurity

Scaffold and develop AI agents — use when asked to create, build, or set up a new AI agent, including agent definition, instructions, tools, guardrails, handoffs, context, RAG pipelines, streaming, API routes, testing, and debugging.

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added October 26, 2026
ai-agentstypescriptgobashsqlrailstestingdebuggingapidatabase

Works with

  • cli
  • api
  • mcp

Security analysis

A100/100

Scanned October 26, 2026

npx -y skills add JoshuaShepherd/my-skills --skill create-agent --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Create Agent?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Create Agent
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/joshuashepherd-create-agent/badge)](https://www.skillsdirectory.com/skills/joshuashepherd-create-agent)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: create-agent
description: Scaffold and develop AI agents — use when asked to create, build, or set up a new AI agent, including agent definition, instructions, tools, guardrails, handoffs, context, RAG pipelines, streaming, API routes, testing, and debugging.
---

Scaffold or develop an agent: $ARGUMENTS

## Before Starting

1. Search OpenAI docs for the latest Agents SDK patterns:
   - Use `mcp__openai-docs__search_openai_docs` with query "Agents SDK agent definition" to confirm current `Agent` constructor and `run()` patterns
   - Use `mcp__openai-docs__search_openai_docs` with query "Agents SDK tools" for tool definition patterns
2. Read existing agent definitions to understand project conventions:
   - `{{AGENTS_DIR}}/chat-coaches/index.ts` — simple agent (tenant-driven prompt, single model)
   - Read 1-2 files in the ai-lab-agent repo at `{{PROJECT_ROOT}}/{{AGENTS_DIR}}/ai-lab/index.ts` for the function-based config pattern (instructions/tools/modelSettings as functions receiving RunContext)
   - `{{PROJECT_ROOT}}/{{AGENTS_DIR}}/writing-assistant/index.ts` — multi-mode agent with per-mode temperature/token configs
   - `{{PROJECT_ROOT}}/{{AGENTS_DIR}}/seo-expert/index.ts` — tool-heavy agent with ordered tool execution
3. Read the shared infrastructure:
   - `{{AGENTS_DIR}}/shared/enhanced-agent-bridge.ts` — bridge class that wraps agent execution
   - `{{AGENTS_DIR}}/shared/types.ts` — ChatKitRequest, ChatKitEvent, WritingContext types

---

## Part 1: Agent Scaffolding

### Step 1: Agent Definition

Create `{{AGENTS_DIR}}/<name>/index.ts`:

```typescript
import { Agent } from '@openai/agents';

export const <name>Agent = new Agent({
  name: '<Display Name>',
  instructions: '<system prompt — or import from instructions.ts>',
  model: '<model-id>',  // gpt-4o, gpt-4o-mini, etc.
  tools: [],  // or import from tools.ts
  modelSettings: {
    temperature: 0.7,
    maxTokens: 2000,
  },
});
```

For complex agents that need runtime adaptation, use the function-based pattern from ai-lab-agent:

```typescript
export const <name>Agent = new Agent({
  name: '<Display Name>',
  instructions: (context: RunContext<MyContext>) => generateInstructions(context),
  tools: (context: RunContext<MyContext>) => getToolsForAgent(context),
  modelSettings: (context: RunContext<MyContext>) => getModelSettings(context),
});
```

### Step 2: Instructions File (if complex)

Create `{{AGENTS_DIR}}/<name>/instructions.ts` with composable instruction sections. Follow the pattern in ai-lab-agent where instructions are split into identity, context, and mode sections.

### Step 3: Tools File (if needed)

Create `{{AGENTS_DIR}}/<name>/tools.ts` using the `tool()` helper from `@openai/agents` with Zod parameter schemas. See the Tools section below for detailed patterns.

### Step 4: API Route

Create `src/app/api/agents/<name>/route.ts`:

```typescript
import { NextRequest } from 'next/server';
import { createServerClient } from '@/lib/supabase/server';
import { createEnhancedAgentBridge } from '@/agents/shared/enhanced-agent-bridge';
import { nonEmpty, maxLength, contentFilter } from '@/agents/shared/guardrails';
import { noErrors } from '@/agents/shared/guardrails';
import { defaultDynamicInstructions } from '@/agents/shared/dynamic-instructions';
import { <name>Agent } from '@/agents/<name>';

export const maxDuration = 60;

export async function POST(req: NextRequest) {
  const supabase = await createServerClient();
  const { data: { user } } = await supabase.auth.getUser();
  if (!user) return new Response('Unauthorized', { status: 401 });

  const bridge = createEnhancedAgentBridge(<name>Agent, {
    agentId: '<name>',
    inputGuardrails: [nonEmpty(), maxLength(10000), contentFilter()],
    outputGuardrails: [nonEmpty(), noErrors()],
    dynamicInstructions: defaultDynamicInstructions,
    enableHandoffs: false,
    tracing: { workflowName: '<name>-agent' },
  });

  return bridge.handleRequest(req);
}
```

### Step 5: Register in Agent Registry (if handoffs needed)

Add to `{{AGENTS_DIR}}/shared/handoffs.ts`:
```typescript
import { <name>Agent } from '@/agents/<name>';
agentRegistry.register('<name>', <name>Agent);
```

### Step 6: Export from Index

Create or update `{{AGENTS_DIR}}/index.ts` to export the new agent.

---

## Part 2: Tools

### Before Starting

1. Search OpenAI docs: `mcp__openai-docs__search_openai_docs` with query "Agents SDK tool definition" for current `tool()` helper signature
2. Read existing implementations:
   - `{{PROJECT_ROOT}}/{{AGENTS_DIR}}/ai-lab/tools/base.ts` — search_books, file_search tools with caching + metrics
   - `{{PROJECT_ROOT}}/{{AGENTS_DIR}}/seo-expert/tools.ts` — 9-tool suite
   - `{{PROJECT_ROOT}}/{{AGENTS_DIR}}/ai-lab/tools/utils.ts` — `executeToolWithMetrics()`, timeout constants
3. Read: `{{PROJECT_ROOT}}/src/services/toolCacheService.ts` — param-based LRU caching with TTL

### Tool Template

```typescript
import { tool } from '@openai/agents';
import { z } from 'zod';
import type { RunContext } from '@openai/agents';

export const myTool = tool({
  name: 'my_tool',
  description: 'Clear, specific description of what this tool does and when the agent should use it.',
  parameters: z.object({
    query: z.string().describe('What to search for'),
    maxResults: z.number().optional().default(5).describe('Maximum results to return'),
  }),
  strict: true,
  execute: async (params, context?: RunContext<MyContext>) => {
    const cacheKey = `my_tool:${params.query}`;
    const cached = await toolCacheService.get(cacheKey);
    if (cached) return cached;

    const { result } = await executeToolWithMetrics('my_tool', async () => {
      return { data: 'result' };
    }, TOOL_TIMEOUTS.MEDIUM);

    toolCacheService.set(cacheKey, result).catch(() => {});
    return result;
  },
});
```

### Patterns by Tool Type

**Search / Retrieval Tools**: Return structured results with title, snippet, url/slug, relevance score. Always include `maxResults`. Gracefully degrade when unavailable.

**Analysis / Computation Tools**: Accept content as string input. Return structured analysis object with scores/categories/suggestions. Include confidence scores.

**API Integration Tools**: Validate API keys before calling. Set appropriate timeouts (SHORT=5s, MEDIUM=15s, LONG=30s). Return user-friendly errors. Never expose API keys.

**Database Query Tools**: Use the service layer (never raw SQL). Scope by `organizationId` for multi-tenant safety. Return only needed fields.

### Tool Registration

**Static** (simple agents):
```typescript
import { myTool } from './tools';
export const myAgent = new Agent({ tools: [myTool, existingTool1] });
```

**Dynamic** (function-based agents):
```typescript
export function getToolsForConfiguration(context: RunContext<MyContext>): Tool[] {
  const tools = [...getBaseTools()];
  if (context.context.someCondition) tools.push(myTool);
  return tools;
}
```

### Tool Rules

- Tool names must be snake_case
- Descriptions should tell the agent WHEN to use the tool, not just what it does
- Parameters must use Zod schemas with `.describe()` on every field
- Always handle errors gracefully — return error objects, never throw
- Cache results when the tool calls external services
- Wrap execution in `executeToolWithMetrics()` for observability
- Tool results are sent to the LLM — keep payloads concise

---

## Part 3: Instructions

### Before Starting

Read the existing instruction architecture:
- `{{PROJECT_ROOT}}/{{AGENTS_DIR}}/ai-lab/instructions/core.ts` — identity + voice markers + failure modes
- `{{PROJECT_ROOT}}/{{AGENTS_DIR}}/ai-lab/instructions/themes.ts` — theme-specific layers
- `{{PROJECT_ROOT}}/{{AGENTS_DIR}}/ai-lab/instructions/modes.ts` — pedagogical modes
- `{{PROJECT_ROOT}}/{{AGENTS_DIR}}/ai-lab/instructions/styles.ts` — interaction styles
- `{{PROJECT_ROOT}}/{{AGENTS_DIR}}/ai-lab/instructions/context.ts` — dynamic user context per run

### Instruction Architecture

Instructions are composed from **layers** concatenated at runtime:

```
STATIC (cached by dimension key)
  Core Identity — who the agent IS, voice markers, failure modes
  + Theme Layer — theological/framework lens
  + Mode Layer — pedagogical approach
  + Style Layer — interaction pattern

DYNAMIC (fresh per run)
  User Context — name, role, language, kairos, engagement
  + Conversation History — continuity from prior turns
  + Page/Content Context — what the user is currently viewing
```

Static sections are cached by key `{theme}-{mode}-{style}` (max 100 entries). Dynamic sections are NEVER cached.

### Writing Instructions

**Core Identity**: Role statement, voice markers (3-5 dimensions), signature elements, failure modes (what NOT to do).

**Theme/Mode/Style Layers**: Each adds a focused paragraph or two. Keep modular — should make sense in any combination.

**Dynamic Context** via `DynamicInstructionsGenerator`:
```typescript
export const myDynamicInstructions: DynamicInstructionsGenerator = async (
  baseInstructions: string,
  context: Record<string, unknown>
) => {
  const sections: string[] = [baseInstructions];
  if (context.userName) {
    sections.push(`## User Context\nYou are speaking with ${context.userName}.`);
  }
  return sections.join('\n\n');
};
```

### Instruction Rules

- Never hardcode tenant-specific names — use `brandConfig` or DB-backed content
- Keep total instruction length under 10,000 tokens
- Voice markers should be descriptive enough for the LLM to calibrate tone
- Failure modes are as important as positive instructions
- For book-related agents, include mandatory retrieval instructions

---

## Part 4: Context

### Context Architecture

```
UI Drawer (client) → AILabNewContextDraft (Zod) → buildRunContractPayload() (validates slugs)
  → RunContract (sent to agent) → buildUserContextInstructions() → Dynamic Instructions
```

### Context Schema

```typescript
export const aiLabNewContextDraftSchema = z.object({
  name: z.string().optional(),
  location: z.string().optional(),
  vocational: z.object({ role: z.string().optional(), context: z.string().optional() }).optional(),
  personality: z.object({ type: z.string().optional(), details: z.string().optional() }).optional(),
  neighborhood: z.object({ description: z.string().optional(), challenges: z.string().optional() }).optional(),
  kairos: z.object({ worldEvents: z.string().optional(), lifeEvents: z.string().optional() }).optional(),
  engagement: z.enum(['exploring', 'learning', 'applying', 'leading']).optional(),
  booksFocus: z.array(z.string()).optional(),
  topicsSelected: z.array(z.string()).optional(),
  language: z.string().default('en'),
});
```

### Adding New Context Fields

1. Add to the Zod schema (`context-schema-template.ts`)
2. Add UI collection in the context drawer
3. Map to RunContract in `assemble-user-context.ts`
4. Render as a context block in `build-user-context-blocks.ts`
5. Use in dynamic instructions

### Context Rules

- Context schemas must be Zod-validated
- Validate book/topic slugs against the catalog — drop unknowns silently
- Dynamic context is NEVER cached
- Never include sensitive user data in context blocks
- User context shapes the conversation; agent content shapes the voice — keep them separate

---

## Part 5: Guardrails

### Input Guardrails

Validate user messages BEFORE they reach the agent:

```typescript
export function myInputGuardrail(): InputGuardrail {
  return {
    name: 'my_guardrail',
    validate: async (message: string) => {
      if (someCondition(message)) {
        return { passed: false, reason: 'Specific reason for rejection' };
      }
      return { passed: true };
    },
  };
}
```

### Output Guardrails

Validate agent responses BEFORE they reach the user. Same interface.

### Built-in Guardrails

| Name | Type | What it does |
|------|------|-------------|
| `nonEmpty()` | Input & Output | Rejects empty/whitespace-only messages |
| `maxLength(n)` | Input | Rejects messages over n characters |
| `contentFilter()` | Input | Blocks XSS patterns |
| `noErrors()` | Output | Detects error indicators in output |

### Guardrail Rules

- Guardrails must be fast — avoid LLM calls
- Input guardrails protect the agent; output guardrails protect the user
- Return clear, user-friendly rejection messages
- Order matters: put cheap checks before expensive ones

---

## Part 6: Handoffs

### Agent Registry

```typescript
import { agentRegistry } from '@/agents/shared/handoffs';
agentRegistry.register('my-agent', myAgent);
agentRegistry.register('expert', expertAgent);
```

### Handoff Decision Logic

```typescript
export const myHandoffHandler: HandoffHandler = async (
  message: string, context: Record<string, unknown>, currentAgent: string
): Promise<HandoffDecision> => {
  const seoKeywords = /\b(seo|search engine|meta tags)\b/i;
  if (seoKeywords.test(message) && currentAgent !== 'seo-expert') {
    return { shouldHandoff: true, targetAgent: 'seo-expert', reason: 'SEO question' };
  }
  return { shouldHandoff: false };
};
```

Enable in the bridge: `enableHandoffs: true`

### Handoff Rules

- Register agents in `agentRegistry` before enabling handoffs
- Use keyword matching, not LLM calls, for handoff decisions
- Prevent circular handoffs — max depth: 3
- Preserve conversation context across handoffs
- Check OpenAI docs for SDK-native handoff support

---

## Part 7: RAG Pipeline

### Architecture

```
User Message → Intent Router (classify query type) → File Search (vector store)
  → Post-Filter + Re-Sort (by user context) → Fallback Search (if needed)
  → Citation Render (file path → book/chapter) → EnrichedResultItem[]
```

### Intent Types

`QUOTE_REQUEST`, `CROSS_BOOK_COMPARISON`, `GLOSSARY_DEFINITION`, `RESEARCH_MODE`, `TOPIC_GUIDE`, `BOOK_SPECIFIC`, `GENERAL_CONVERSATION`

### Book Fidelity Contract

- Cache skipping for book-related requests
- Temperature override: 0.2-0.3 for book fidelity
- Mandatory tool usage: always use file_search before answering book questions
- Citation requirement: output guardrail checks for citations

### RAG Rules

- Gracefully degrade when vector store is unavailable
- Post-filter by user context (booksFocus, topicsSelected)
- Include fallback search with broadened query
- Citation rendering must be deterministic
- Keep retrieval payloads concise

---

## Part 8: Streaming

### Dual Stream Formats

**SSE** (`text/event-stream`): Primary format. Events as `data: {json}\n\n`.

**Plain Text** (`text/plain`): Vercel AI SDK compatibility. Triggered by `?format=text`. Appends `__AILAB_SESSION_ID__:uuid`.

### ChatKit Event Types

`text_delta`, `tool_call`, `tool_complete`, `image`, `done`, `error`, `progress`

### Session Continuity

- Request carries `conversationId`
- Response `done` event includes `sessionId`
- Client captures session ID for multi-turn continuity

### Streaming Rules

- Always include `Cache-Control: no-cache` and `Connection: keep-alive`
- The `done` event MUST include `sessionId`
- Window messages to prevent token overflow (max 50)
- Never buffer entire response before streaming

---

## Part 9: Testing

### Unit Tests (Vitest)

Test tools, instructions, router, citations, and guardrails:
```bash
pnpm test:run -- agents
```

### Smoke Tests

Hit running endpoints to assert tool invocation and citation presence.

### E2E Tests (Playwright)

```bash
pnpm test:e2e -- agents
```

### Testing Rules

- Unit tests must not call external APIs — mock them
- Test both happy paths and error cases
- For retrieval tests, assert tool invocation AND citation presence
- Keep test files focused — one file per concern

---

## Part 10: Debugging

### Trace Event Types

`start`, `tool_call`, `tool_complete`, `handoff`, `stream_delta`, `complete`, `error`

### Debug Flags

```bash
AI_LAB_DEBUG_TOOLS=true
AI_LAB_DEBUG_CONTEXT=true
AI_LAB_DEBUG_INSTRUCTIONS=true
AI_LAB_DEBUG_RETRIEVAL=true
```

### Analysis Patterns

- **Slow responses**: Query traces with high duration, check tool_call durations
- **Tool failures**: Check error events, validate tool params, check external service availability
- **Guardrail rejections**: Check error events with guardrail name and reason

---

## Top-Level Rules

- Use `brandConfig` for any tenant-specific strings — never hardcode
- Always wire through EnhancedAgentBridge for guardrails, tracing, and metrics
- Use streaming (SSE) responses via the bridge — never return plain JSON for chat
- Follow the Result<T> pattern in any service calls within tools
- Check OpenAI docs MCP for any SDK changes before generating agent code

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.