feat: build an AI agent from 0 to 1 -- 11 progressive sessions
- 11 sessions from basic agent loop to autonomous teams - Python MVP implementations for each session - Mental-model-first docs in en/zh/ja - Interactive web platform with step-through visualizations - Incremental architecture: each session adds one mechanism
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
---
|
||||
name: agent-builder
|
||||
description: |
|
||||
Design and build AI agents for any domain. Use when users:
|
||||
(1) ask to "create an agent", "build an assistant", or "design an AI system"
|
||||
(2) want to understand agent architecture, agentic patterns, or autonomous AI
|
||||
(3) need help with capabilities, subagents, planning, or skill mechanisms
|
||||
(4) ask about Claude Code, Cursor, or similar agent internals
|
||||
(5) want to build agents for business, research, creative, or operational tasks
|
||||
Keywords: agent, assistant, autonomous, workflow, tool use, multi-step, orchestration
|
||||
---
|
||||
|
||||
# Agent Builder
|
||||
|
||||
Build AI agents for any domain - customer service, research, operations, creative work, or specialized business processes.
|
||||
|
||||
## The Core Philosophy
|
||||
|
||||
> **The model already knows how to be an agent. Your job is to get out of the way.**
|
||||
|
||||
An agent is not complex engineering. It's a simple loop that invites the model to act:
|
||||
|
||||
```
|
||||
LOOP:
|
||||
Model sees: context + available capabilities
|
||||
Model decides: act or respond
|
||||
If act: execute capability, add result, continue
|
||||
If respond: return to user
|
||||
```
|
||||
|
||||
**That's it.** The magic isn't in the code - it's in the model. Your code just provides the opportunity.
|
||||
|
||||
## The Three Elements
|
||||
|
||||
### 1. Capabilities (What can it DO?)
|
||||
|
||||
Atomic actions the agent can perform: search, read, create, send, query, modify.
|
||||
|
||||
**Design principle**: Start with 3-5 capabilities. Add more only when the agent consistently fails because a capability is missing.
|
||||
|
||||
### 2. Knowledge (What does it KNOW?)
|
||||
|
||||
Domain expertise injected on-demand: policies, workflows, best practices, schemas.
|
||||
|
||||
**Design principle**: Make knowledge available, not mandatory. Load it when relevant, not upfront.
|
||||
|
||||
### 3. Context (What has happened?)
|
||||
|
||||
The conversation history - the thread connecting actions into coherent behavior.
|
||||
|
||||
**Design principle**: Context is precious. Isolate noisy subtasks. Truncate verbose outputs. Protect clarity.
|
||||
|
||||
## Agent Design Thinking
|
||||
|
||||
Before building, understand:
|
||||
|
||||
- **Purpose**: What should this agent accomplish?
|
||||
- **Domain**: What world does it operate in? (customer service, research, operations, creative...)
|
||||
- **Capabilities**: What 3-5 actions are essential?
|
||||
- **Knowledge**: What expertise does it need access to?
|
||||
- **Trust**: What decisions can you delegate to the model?
|
||||
|
||||
**CRITICAL**: Trust the model. Don't over-engineer. Don't pre-specify workflows. Give it capabilities and let it reason.
|
||||
|
||||
## Progressive Complexity
|
||||
|
||||
Start simple. Add complexity only when real usage reveals the need:
|
||||
|
||||
| Level | What to add | When to add it |
|
||||
|-------|-------------|----------------|
|
||||
| Basic | 3-5 capabilities | Always start here |
|
||||
| Planning | Progress tracking | Multi-step tasks lose coherence |
|
||||
| Subagents | Isolated child agents | Exploration pollutes context |
|
||||
| Skills | On-demand knowledge | Domain expertise needed |
|
||||
|
||||
**Most agents never need to go beyond Level 2.**
|
||||
|
||||
## Domain Examples
|
||||
|
||||
**Business**: CRM queries, email, calendar, approvals
|
||||
**Research**: Database search, document analysis, citations
|
||||
**Operations**: Monitoring, tickets, notifications, escalation
|
||||
**Creative**: Asset generation, editing, collaboration, review
|
||||
|
||||
The pattern is universal. Only the capabilities change.
|
||||
|
||||
## Key Principles
|
||||
|
||||
1. **The model IS the agent** - Code just runs the loop
|
||||
2. **Capabilities enable** - What it CAN do
|
||||
3. **Knowledge informs** - What it KNOWS how to do
|
||||
4. **Constraints focus** - Limits create clarity
|
||||
5. **Trust liberates** - Let the model reason
|
||||
6. **Iteration reveals** - Start minimal, evolve from usage
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Pattern | Problem | Solution |
|
||||
|---------|---------|----------|
|
||||
| Over-engineering | Complexity before need | Start simple |
|
||||
| Too many capabilities | Model confusion | 3-5 to start |
|
||||
| Rigid workflows | Can't adapt | Let model decide |
|
||||
| Front-loaded knowledge | Context bloat | Load on-demand |
|
||||
| Micromanagement | Undercuts intelligence | Trust the model |
|
||||
|
||||
## Resources
|
||||
|
||||
**Philosophy & Theory**:
|
||||
- `references/agent-philosophy.md` - Deep dive into why agents work
|
||||
|
||||
**Implementation**:
|
||||
- `references/minimal-agent.py` - Complete working agent (~80 lines)
|
||||
- `references/tool-templates.py` - Capability definitions
|
||||
- `references/subagent-pattern.py` - Context isolation
|
||||
|
||||
**Scaffolding**:
|
||||
- `scripts/init_agent.py` - Generate new agent projects
|
||||
|
||||
## The Agent Mindset
|
||||
|
||||
**From**: "How do I make the system do X?"
|
||||
**To**: "How do I enable the model to do X?"
|
||||
|
||||
**From**: "What's the workflow for this task?"
|
||||
**To**: "What capabilities would help accomplish this?"
|
||||
|
||||
The best agent code is almost boring. Simple loops. Clear capabilities. Clean context. The magic isn't in the code.
|
||||
|
||||
**Give the model capabilities and knowledge. Trust it to figure out the rest.**
|
||||
@@ -0,0 +1,144 @@
|
||||
# The Philosophy of Agents
|
||||
|
||||
> **The model already knows how to be an agent. Your job is to get out of the way.**
|
||||
|
||||
## The Fundamental Insight
|
||||
|
||||
Strip away every framework, every library, every architectural pattern. What remains?
|
||||
|
||||
A loop. A model. An invitation to act.
|
||||
|
||||
The agent is not the code. The agent is the model itself - a vast neural network trained on humanity's collective problem-solving, reasoning, and tool use. The code merely provides the opportunity for the model to express its agency.
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Most agent implementations fail not from too little engineering, but from too much. They constrain. They prescribe. They second-guess the very intelligence they're trying to leverage.
|
||||
|
||||
Consider: The model has been trained on millions of examples of problem-solving. It has seen how experts approach complex tasks, how tools are used, how plans are formed and revised. This knowledge is already there, encoded in billions of parameters.
|
||||
|
||||
Your job is not to teach it how to think. Your job is to give it the means to act.
|
||||
|
||||
## The Three Elements
|
||||
|
||||
### 1. Capabilities (Tools)
|
||||
|
||||
Capabilities answer: **What can the agent DO?**
|
||||
|
||||
They are the hands of the model - its ability to affect the world. Without capabilities, the model can only speak. With them, it can act.
|
||||
|
||||
**The design principle**: Each capability should be atomic, clear, and well-described. The model needs to understand what each capability does, but not how to use them in sequence - it will figure that out.
|
||||
|
||||
**Common mistake**: Too many capabilities. The model gets confused, starts using the wrong ones, or paralyzed by choice. Start with 3-5. Add more only when the model consistently fails to accomplish tasks because a capability is missing.
|
||||
|
||||
### 2. Knowledge (Skills)
|
||||
|
||||
Knowledge answers: **What does the agent KNOW?**
|
||||
|
||||
This is domain expertise - the specialized understanding that turns a general assistant into a domain expert. A customer service agent needs to know company policies. A research agent needs to know methodology. A creative agent needs to know style guidelines.
|
||||
|
||||
**The design principle**: Inject knowledge on-demand, not upfront. The model doesn't need to know everything at once - only what's relevant to the current task. Progressive disclosure preserves context for what matters.
|
||||
|
||||
**Common mistake**: Front-loading all possible knowledge into the system prompt. This wastes context, confuses the model, and makes every interaction expensive. Instead, make knowledge available but not mandatory.
|
||||
|
||||
### 3. Context (The Conversation)
|
||||
|
||||
Context is the memory of the interaction - what has been said, what has been tried, what has been learned. It's the thread that connects individual actions into coherent behavior.
|
||||
|
||||
**The design principle**: Context is precious. Protect it. Isolate subtasks that generate noise. Truncate outputs that exceed usefulness. Summarize when history grows long.
|
||||
|
||||
**Common mistake**: Letting context grow unbounded, filling it with exploration details, failed attempts, and verbose tool outputs. Eventually the model can't find the signal in the noise.
|
||||
|
||||
## The Universal Pattern
|
||||
|
||||
Every effective agent - regardless of domain, framework, or implementation - follows the same pattern:
|
||||
|
||||
```
|
||||
LOOP:
|
||||
Model sees: conversation history + available capabilities
|
||||
Model decides: act or respond
|
||||
If act: capability executed, result added to context, loop continues
|
||||
If respond: answer returned, loop ends
|
||||
```
|
||||
|
||||
This is not a simplification. This is the actual architecture. Everything else is optimization.
|
||||
|
||||
## Designing for Agency
|
||||
|
||||
### Trust the Model
|
||||
|
||||
The most important principle: **trust the model**.
|
||||
|
||||
Don't try to anticipate every edge case. Don't build elaborate decision trees. Don't pre-specify the workflow.
|
||||
|
||||
The model is better at reasoning than any rule system you could write. Your conditional logic will fail on edge cases. The model will reason through them.
|
||||
|
||||
**Give the model capabilities and knowledge. Let it figure out how to use them.**
|
||||
|
||||
### Constraints Enable
|
||||
|
||||
This seems paradoxical, but constraints don't limit agents - they focus them.
|
||||
|
||||
A todo list with "only one task in progress" forces sequential focus. A subagent with "read-only access" prevents accidental modifications. A response with "under 100 words" demands clarity.
|
||||
|
||||
The best constraints are those that prevent the model from getting lost, not those that micromanage its approach.
|
||||
|
||||
### Progressive Complexity
|
||||
|
||||
Never build everything upfront.
|
||||
|
||||
```
|
||||
Level 0: Model + one capability
|
||||
Level 1: Model + 3-5 capabilities
|
||||
Level 2: Model + capabilities + planning
|
||||
Level 3: Model + capabilities + planning + subagents
|
||||
Level 4: Model + capabilities + planning + subagents + skills
|
||||
```
|
||||
|
||||
Start at the lowest level that might work. Move up only when real usage reveals the need. Most agents never need to go beyond Level 2.
|
||||
|
||||
## The Agent Mindset
|
||||
|
||||
Building agents requires a shift in thinking:
|
||||
|
||||
**From**: "How do I make the system do X?"
|
||||
**To**: "How do I enable the model to do X?"
|
||||
|
||||
**From**: "What should happen when the user says Y?"
|
||||
**To**: "What capabilities would help address Y?"
|
||||
|
||||
**From**: "What's the workflow for this task?"
|
||||
**To**: "What does the model need to figure out the workflow?"
|
||||
|
||||
The best agent code is almost boring. Simple loops. Clear capability definitions. Clean context management. The magic isn't in the code - it's in the model.
|
||||
|
||||
## Philosophical Foundations
|
||||
|
||||
### The Model as Emergent Agent
|
||||
|
||||
Language models trained on human text have learned not just language, but patterns of thought. They've absorbed how humans approach problems, use tools, and accomplish goals. This is emergent agency - not programmed, but learned.
|
||||
|
||||
When you give a model capabilities, you're not teaching it to be an agent. You're giving it permission to express the agency it already has.
|
||||
|
||||
### The Loop as Liberation
|
||||
|
||||
The agent loop is deceptively simple: get response, check for tool use, execute, repeat. But this simplicity is its power.
|
||||
|
||||
The loop doesn't constrain the model to particular sequences. It doesn't enforce specific workflows. It simply says: "You have capabilities. Use them as you see fit. I'll execute what you request and show you the results."
|
||||
|
||||
This is liberation, not limitation.
|
||||
|
||||
### Capabilities as Expression
|
||||
|
||||
Each capability you provide is a form of expression for the model. "Read file" lets it see. "Write file" lets it create. "Search" lets it explore. "Send message" lets it communicate.
|
||||
|
||||
The art of agent design is choosing which forms of expression to enable. Too few, and the model is mute. Too many, and it speaks in tongues.
|
||||
|
||||
## Conclusion
|
||||
|
||||
The agent is the model. The code is just the loop. Your job is to get out of the way.
|
||||
|
||||
Give the model clear capabilities. Make knowledge available when needed. Protect the context from noise. Trust the model to figure out the rest.
|
||||
|
||||
That's it. That's the philosophy.
|
||||
|
||||
Everything else is refinement.
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Minimal Agent Template - Copy and customize this.
|
||||
|
||||
This is the simplest possible working agent (~80 lines).
|
||||
It has everything you need: 3 tools + loop.
|
||||
|
||||
Usage:
|
||||
1. Set ANTHROPIC_API_KEY environment variable
|
||||
2. python minimal-agent.py
|
||||
3. Type commands, 'q' to quit
|
||||
"""
|
||||
|
||||
from anthropic import Anthropic
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
# Configuration
|
||||
client = Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
|
||||
MODEL = os.getenv("MODEL_NAME", "claude-sonnet-4-20250514")
|
||||
WORKDIR = Path.cwd()
|
||||
|
||||
# System prompt - keep it simple
|
||||
SYSTEM = f"""You are a coding agent at {WORKDIR}.
|
||||
|
||||
Rules:
|
||||
- Use tools to complete tasks
|
||||
- Prefer action over explanation
|
||||
- Summarize what you did when done"""
|
||||
|
||||
# Minimal tool set - add more as needed
|
||||
TOOLS = [
|
||||
{
|
||||
"name": "bash",
|
||||
"description": "Run shell command",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"command": {"type": "string"}},
|
||||
"required": ["command"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read file contents",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"path": {"type": "string"}},
|
||||
"required": ["path"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "write_file",
|
||||
"description": "Write content to file",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {"type": "string"},
|
||||
"content": {"type": "string"}
|
||||
},
|
||||
"required": ["path", "content"]
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def execute_tool(name: str, args: dict) -> str:
|
||||
"""Execute a tool and return result."""
|
||||
if name == "bash":
|
||||
try:
|
||||
r = subprocess.run(
|
||||
args["command"], shell=True, cwd=WORKDIR,
|
||||
capture_output=True, text=True, timeout=60
|
||||
)
|
||||
return (r.stdout + r.stderr).strip() or "(empty)"
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Error: Timeout"
|
||||
|
||||
if name == "read_file":
|
||||
try:
|
||||
return (WORKDIR / args["path"]).read_text()[:50000]
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
if name == "write_file":
|
||||
try:
|
||||
p = WORKDIR / args["path"]
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(args["content"])
|
||||
return f"Wrote {len(args['content'])} bytes to {args['path']}"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
return f"Unknown tool: {name}"
|
||||
|
||||
|
||||
def agent(prompt: str, history: list = None) -> str:
|
||||
"""Run the agent loop."""
|
||||
if history is None:
|
||||
history = []
|
||||
|
||||
history.append({"role": "user", "content": prompt})
|
||||
|
||||
while True:
|
||||
response = client.messages.create(
|
||||
model=MODEL,
|
||||
system=SYSTEM,
|
||||
messages=history,
|
||||
tools=TOOLS,
|
||||
max_tokens=8000,
|
||||
)
|
||||
|
||||
# Build assistant message
|
||||
history.append({"role": "assistant", "content": response.content})
|
||||
|
||||
# If no tool calls, return text
|
||||
if response.stop_reason != "tool_use":
|
||||
return "".join(b.text for b in response.content if hasattr(b, "text"))
|
||||
|
||||
# Execute tools
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
print(f"> {block.name}: {block.input}")
|
||||
output = execute_tool(block.name, block.input)
|
||||
print(f" {output[:100]}...")
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output
|
||||
})
|
||||
|
||||
history.append({"role": "user", "content": results})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"Minimal Agent - {WORKDIR}")
|
||||
print("Type 'q' to quit.\n")
|
||||
|
||||
history = []
|
||||
while True:
|
||||
try:
|
||||
query = input(">> ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
break
|
||||
if query in ("q", "quit", "exit", ""):
|
||||
break
|
||||
print(agent(query, history))
|
||||
print()
|
||||
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
Subagent Pattern - How to implement Task tool for context isolation.
|
||||
|
||||
The key insight: spawn child agents with ISOLATED context to prevent
|
||||
"context pollution" where exploration details fill up the main conversation.
|
||||
"""
|
||||
|
||||
import time
|
||||
import sys
|
||||
|
||||
# Assuming client, MODEL, execute_tool are defined elsewhere
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# AGENT TYPE REGISTRY
|
||||
# =============================================================================
|
||||
|
||||
AGENT_TYPES = {
|
||||
# Explore: Read-only, for searching and analyzing
|
||||
"explore": {
|
||||
"description": "Read-only agent for exploring code, finding files, searching",
|
||||
"tools": ["bash", "read_file"], # No write access!
|
||||
"prompt": "You are an exploration agent. Search and analyze, but NEVER modify files. Return a concise summary of what you found.",
|
||||
},
|
||||
|
||||
# Code: Full-powered, for implementation
|
||||
"code": {
|
||||
"description": "Full agent for implementing features and fixing bugs",
|
||||
"tools": "*", # All tools
|
||||
"prompt": "You are a coding agent. Implement the requested changes efficiently. Return a summary of what you changed.",
|
||||
},
|
||||
|
||||
# Plan: Read-only, for design work
|
||||
"plan": {
|
||||
"description": "Planning agent for designing implementation strategies",
|
||||
"tools": ["bash", "read_file"], # Read-only
|
||||
"prompt": "You are a planning agent. Analyze the codebase and output a numbered implementation plan. Do NOT make any changes.",
|
||||
},
|
||||
|
||||
# Add your own types here...
|
||||
# "test": {
|
||||
# "description": "Testing agent for running and analyzing tests",
|
||||
# "tools": ["bash", "read_file"],
|
||||
# "prompt": "Run tests and report results. Don't modify code.",
|
||||
# },
|
||||
}
|
||||
|
||||
|
||||
def get_agent_descriptions() -> str:
|
||||
"""Generate descriptions for Task tool schema."""
|
||||
return "\n".join(
|
||||
f"- {name}: {cfg['description']}"
|
||||
for name, cfg in AGENT_TYPES.items()
|
||||
)
|
||||
|
||||
|
||||
def get_tools_for_agent(agent_type: str, base_tools: list) -> list:
|
||||
"""
|
||||
Filter tools based on agent type.
|
||||
|
||||
'*' means all base tools.
|
||||
Otherwise, whitelist specific tool names.
|
||||
|
||||
Note: Subagents don't get Task tool to prevent infinite recursion.
|
||||
"""
|
||||
allowed = AGENT_TYPES.get(agent_type, {}).get("tools", "*")
|
||||
|
||||
if allowed == "*":
|
||||
return base_tools # All base tools, but NOT Task
|
||||
|
||||
return [t for t in base_tools if t["name"] in allowed]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TASK TOOL DEFINITION
|
||||
# =============================================================================
|
||||
|
||||
TASK_TOOL = {
|
||||
"name": "Task",
|
||||
"description": f"""Spawn a subagent for a focused subtask.
|
||||
|
||||
Subagents run in ISOLATED context - they don't see parent's history.
|
||||
Use this to keep the main conversation clean.
|
||||
|
||||
Agent types:
|
||||
{get_agent_descriptions()}
|
||||
|
||||
Example uses:
|
||||
- Task(explore): "Find all files using the auth module"
|
||||
- Task(plan): "Design a migration strategy for the database"
|
||||
- Task(code): "Implement the user registration form"
|
||||
""",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Short task name (3-5 words) for progress display"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string",
|
||||
"description": "Detailed instructions for the subagent"
|
||||
},
|
||||
"agent_type": {
|
||||
"type": "string",
|
||||
"enum": list(AGENT_TYPES.keys()),
|
||||
"description": "Type of agent to spawn"
|
||||
},
|
||||
},
|
||||
"required": ["description", "prompt", "agent_type"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SUBAGENT EXECUTION
|
||||
# =============================================================================
|
||||
|
||||
def run_task(description: str, prompt: str, agent_type: str,
|
||||
client, model: str, workdir, base_tools: list, execute_tool) -> str:
|
||||
"""
|
||||
Execute a subagent task with isolated context.
|
||||
|
||||
Key concepts:
|
||||
1. ISOLATED HISTORY - subagent starts fresh, no parent context
|
||||
2. FILTERED TOOLS - based on agent type permissions
|
||||
3. AGENT-SPECIFIC PROMPT - specialized behavior
|
||||
4. RETURNS SUMMARY ONLY - parent sees just the final result
|
||||
|
||||
Args:
|
||||
description: Short name for progress display
|
||||
prompt: Detailed instructions for subagent
|
||||
agent_type: Key from AGENT_TYPES
|
||||
client: Anthropic client
|
||||
model: Model to use
|
||||
workdir: Working directory
|
||||
base_tools: List of tool definitions
|
||||
execute_tool: Function to execute tools
|
||||
|
||||
Returns:
|
||||
Final text output from subagent
|
||||
"""
|
||||
if agent_type not in AGENT_TYPES:
|
||||
return f"Error: Unknown agent type '{agent_type}'"
|
||||
|
||||
config = AGENT_TYPES[agent_type]
|
||||
|
||||
# Agent-specific system prompt
|
||||
sub_system = f"""You are a {agent_type} subagent at {workdir}.
|
||||
|
||||
{config["prompt"]}
|
||||
|
||||
Complete the task and return a clear, concise summary."""
|
||||
|
||||
# Filtered tools for this agent type
|
||||
sub_tools = get_tools_for_agent(agent_type, base_tools)
|
||||
|
||||
# KEY: ISOLATED message history!
|
||||
# The subagent starts fresh, doesn't see parent's conversation
|
||||
sub_messages = [{"role": "user", "content": prompt}]
|
||||
|
||||
# Progress display
|
||||
print(f" [{agent_type}] {description}")
|
||||
start = time.time()
|
||||
tool_count = 0
|
||||
|
||||
# Run the same agent loop (but silently)
|
||||
while True:
|
||||
response = client.messages.create(
|
||||
model=model,
|
||||
system=sub_system,
|
||||
messages=sub_messages,
|
||||
tools=sub_tools,
|
||||
max_tokens=8000,
|
||||
)
|
||||
|
||||
# Check if done
|
||||
if response.stop_reason != "tool_use":
|
||||
break
|
||||
|
||||
# Execute tools
|
||||
tool_calls = [b for b in response.content if b.type == "tool_use"]
|
||||
results = []
|
||||
|
||||
for tc in tool_calls:
|
||||
tool_count += 1
|
||||
output = execute_tool(tc.name, tc.input)
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": tc.id,
|
||||
"content": output
|
||||
})
|
||||
|
||||
# Update progress (in-place on same line)
|
||||
elapsed = time.time() - start
|
||||
sys.stdout.write(
|
||||
f"\r [{agent_type}] {description} ... {tool_count} tools, {elapsed:.1f}s"
|
||||
)
|
||||
sys.stdout.flush()
|
||||
|
||||
sub_messages.append({"role": "assistant", "content": response.content})
|
||||
sub_messages.append({"role": "user", "content": results})
|
||||
|
||||
# Final progress update
|
||||
elapsed = time.time() - start
|
||||
sys.stdout.write(
|
||||
f"\r [{agent_type}] {description} - done ({tool_count} tools, {elapsed:.1f}s)\n"
|
||||
)
|
||||
|
||||
# Extract and return ONLY the final text
|
||||
# This is what the parent agent sees - a clean summary
|
||||
for block in response.content:
|
||||
if hasattr(block, "text"):
|
||||
return block.text
|
||||
|
||||
return "(subagent returned no text)"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# USAGE EXAMPLE
|
||||
# =============================================================================
|
||||
|
||||
"""
|
||||
# In your main agent's execute_tool function:
|
||||
|
||||
def execute_tool(name: str, args: dict) -> str:
|
||||
if name == "Task":
|
||||
return run_task(
|
||||
description=args["description"],
|
||||
prompt=args["prompt"],
|
||||
agent_type=args["agent_type"],
|
||||
client=client,
|
||||
model=MODEL,
|
||||
workdir=WORKDIR,
|
||||
base_tools=BASE_TOOLS,
|
||||
execute_tool=execute_tool # Pass self for recursion
|
||||
)
|
||||
# ... other tools ...
|
||||
|
||||
|
||||
# In your TOOLS list:
|
||||
TOOLS = BASE_TOOLS + [TASK_TOOL]
|
||||
"""
|
||||
@@ -0,0 +1,271 @@
|
||||
"""
|
||||
Tool Templates - Copy and customize these for your agent.
|
||||
|
||||
Each tool needs:
|
||||
1. Definition (JSON schema for the model)
|
||||
2. Implementation (Python function)
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
WORKDIR = Path.cwd()
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TOOL DEFINITIONS (for TOOLS list)
|
||||
# =============================================================================
|
||||
|
||||
BASH_TOOL = {
|
||||
"name": "bash",
|
||||
"description": "Run a shell command. Use for: ls, find, grep, git, npm, python, etc.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "The shell command to execute"
|
||||
}
|
||||
},
|
||||
"required": ["command"],
|
||||
},
|
||||
}
|
||||
|
||||
READ_FILE_TOOL = {
|
||||
"name": "read_file",
|
||||
"description": "Read file contents. Returns UTF-8 text.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Relative path to the file"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max lines to read (default: all)"
|
||||
},
|
||||
},
|
||||
"required": ["path"],
|
||||
},
|
||||
}
|
||||
|
||||
WRITE_FILE_TOOL = {
|
||||
"name": "write_file",
|
||||
"description": "Write content to a file. Creates parent directories if needed.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Relative path for the file"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Content to write"
|
||||
},
|
||||
},
|
||||
"required": ["path", "content"],
|
||||
},
|
||||
}
|
||||
|
||||
EDIT_FILE_TOOL = {
|
||||
"name": "edit_file",
|
||||
"description": "Replace exact text in a file. Use for surgical edits.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Relative path to the file"
|
||||
},
|
||||
"old_text": {
|
||||
"type": "string",
|
||||
"description": "Exact text to find (must match precisely)"
|
||||
},
|
||||
"new_text": {
|
||||
"type": "string",
|
||||
"description": "Replacement text"
|
||||
},
|
||||
},
|
||||
"required": ["path", "old_text", "new_text"],
|
||||
},
|
||||
}
|
||||
|
||||
TODO_WRITE_TOOL = {
|
||||
"name": "TodoWrite",
|
||||
"description": "Update the task list. Use to plan and track progress.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"description": "Complete list of tasks",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {"type": "string", "description": "Task description"},
|
||||
"status": {"type": "string", "enum": ["pending", "in_progress", "completed"]},
|
||||
"activeForm": {"type": "string", "description": "Present tense, e.g. 'Reading files'"},
|
||||
},
|
||||
"required": ["content", "status", "activeForm"],
|
||||
},
|
||||
}
|
||||
},
|
||||
"required": ["items"],
|
||||
},
|
||||
}
|
||||
|
||||
TASK_TOOL_TEMPLATE = """
|
||||
# Generate dynamically with agent types
|
||||
TASK_TOOL = {
|
||||
"name": "Task",
|
||||
"description": f"Spawn a subagent for a focused subtask.\\n\\nAgent types:\\n{get_agent_descriptions()}",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {"type": "string", "description": "Short task name (3-5 words)"},
|
||||
"prompt": {"type": "string", "description": "Detailed instructions"},
|
||||
"agent_type": {"type": "string", "enum": list(AGENT_TYPES.keys())},
|
||||
},
|
||||
"required": ["description", "prompt", "agent_type"],
|
||||
},
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# TOOL IMPLEMENTATIONS
|
||||
# =============================================================================
|
||||
|
||||
def safe_path(p: str) -> Path:
|
||||
"""
|
||||
Security: Ensure path stays within workspace.
|
||||
Prevents ../../../etc/passwd attacks.
|
||||
"""
|
||||
path = (WORKDIR / p).resolve()
|
||||
if not path.is_relative_to(WORKDIR):
|
||||
raise ValueError(f"Path escapes workspace: {p}")
|
||||
return path
|
||||
|
||||
|
||||
def run_bash(command: str) -> str:
|
||||
"""
|
||||
Execute shell command with safety checks.
|
||||
|
||||
Safety features:
|
||||
- Blocks obviously dangerous commands
|
||||
- 60 second timeout
|
||||
- Output truncated to 50KB
|
||||
"""
|
||||
dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
|
||||
if any(d in command for d in dangerous):
|
||||
return "Error: Dangerous command blocked"
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
shell=True,
|
||||
cwd=WORKDIR,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=60
|
||||
)
|
||||
output = (result.stdout + result.stderr).strip()
|
||||
return output[:50000] if output else "(no output)"
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Error: Command timed out (60s)"
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
def run_read_file(path: str, limit: int = None) -> str:
|
||||
"""
|
||||
Read file contents with optional line limit.
|
||||
|
||||
Features:
|
||||
- Safe path resolution
|
||||
- Optional line limit for large files
|
||||
- Output truncated to 50KB
|
||||
"""
|
||||
try:
|
||||
text = safe_path(path).read_text()
|
||||
lines = text.splitlines()
|
||||
|
||||
if limit and limit < len(lines):
|
||||
lines = lines[:limit]
|
||||
lines.append(f"... ({len(text.splitlines()) - limit} more lines)")
|
||||
|
||||
return "\n".join(lines)[:50000]
|
||||
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
def run_write_file(path: str, content: str) -> str:
|
||||
"""
|
||||
Write content to file, creating parent directories if needed.
|
||||
|
||||
Features:
|
||||
- Safe path resolution
|
||||
- Auto-creates parent directories
|
||||
- Returns byte count for confirmation
|
||||
"""
|
||||
try:
|
||||
fp = safe_path(path)
|
||||
fp.parent.mkdir(parents=True, exist_ok=True)
|
||||
fp.write_text(content)
|
||||
return f"Wrote {len(content)} bytes to {path}"
|
||||
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
def run_edit_file(path: str, old_text: str, new_text: str) -> str:
|
||||
"""
|
||||
Replace exact text in a file (surgical edit).
|
||||
|
||||
Features:
|
||||
- Exact string matching (not regex)
|
||||
- Only replaces first occurrence (safety)
|
||||
- Clear error if text not found
|
||||
"""
|
||||
try:
|
||||
fp = safe_path(path)
|
||||
content = fp.read_text()
|
||||
|
||||
if old_text not in content:
|
||||
return f"Error: Text not found in {path}"
|
||||
|
||||
new_content = content.replace(old_text, new_text, 1)
|
||||
fp.write_text(new_content)
|
||||
return f"Edited {path}"
|
||||
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DISPATCHER PATTERN
|
||||
# =============================================================================
|
||||
|
||||
def execute_tool(name: str, args: dict) -> str:
|
||||
"""
|
||||
Dispatch tool call to implementation.
|
||||
|
||||
This pattern makes it easy to add new tools:
|
||||
1. Add definition to TOOLS list
|
||||
2. Add implementation function
|
||||
3. Add case to this dispatcher
|
||||
"""
|
||||
if name == "bash":
|
||||
return run_bash(args["command"])
|
||||
if name == "read_file":
|
||||
return run_read_file(args["path"], args.get("limit"))
|
||||
if name == "write_file":
|
||||
return run_write_file(args["path"], args["content"])
|
||||
if name == "edit_file":
|
||||
return run_edit_file(args["path"], args["old_text"], args["new_text"])
|
||||
# Add more tools here...
|
||||
return f"Unknown tool: {name}"
|
||||
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Agent Scaffold Script - Create a new agent project with best practices.
|
||||
|
||||
Usage:
|
||||
python init_agent.py <agent-name> [--level 0-4] [--path <output-dir>]
|
||||
|
||||
Examples:
|
||||
python init_agent.py my-agent # Level 1 (4 tools)
|
||||
python init_agent.py my-agent --level 0 # Minimal (bash only)
|
||||
python init_agent.py my-agent --level 2 # With TodoWrite
|
||||
python init_agent.py my-agent --path ./bots # Custom output directory
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Agent templates for each level
|
||||
TEMPLATES = {
|
||||
0: '''#!/usr/bin/env python3
|
||||
"""
|
||||
Level 0 Agent - Bash is All You Need (~50 lines)
|
||||
|
||||
Core insight: One tool (bash) can do everything.
|
||||
Subagents via self-recursion: python {name}.py "subtask"
|
||||
"""
|
||||
|
||||
from anthropic import Anthropic
|
||||
from dotenv import load_dotenv
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
load_dotenv()
|
||||
|
||||
client = Anthropic(
|
||||
api_key=os.getenv("ANTHROPIC_API_KEY"),
|
||||
base_url=os.getenv("ANTHROPIC_BASE_URL")
|
||||
)
|
||||
MODEL = os.getenv("MODEL_NAME", "claude-sonnet-4-20250514")
|
||||
|
||||
SYSTEM = """You are a coding agent. Use bash for everything:
|
||||
- Read: cat, grep, find, ls
|
||||
- Write: echo 'content' > file
|
||||
- Subagent: python {name}.py "subtask"
|
||||
"""
|
||||
|
||||
TOOL = [{{
|
||||
"name": "bash",
|
||||
"description": "Execute shell command",
|
||||
"input_schema": {{"type": "object", "properties": {{"command": {{"type": "string"}}}}, "required": ["command"]}}
|
||||
}}]
|
||||
|
||||
def run(prompt, history=[]):
|
||||
history.append({{"role": "user", "content": prompt}})
|
||||
while True:
|
||||
r = client.messages.create(model=MODEL, system=SYSTEM, messages=history, tools=TOOL, max_tokens=8000)
|
||||
history.append({{"role": "assistant", "content": r.content}})
|
||||
if r.stop_reason != "tool_use":
|
||||
return "".join(b.text for b in r.content if hasattr(b, "text"))
|
||||
results = []
|
||||
for b in r.content:
|
||||
if b.type == "tool_use":
|
||||
print(f"> {{b.input['command']}}")
|
||||
try:
|
||||
out = subprocess.run(b.input["command"], shell=True, capture_output=True, text=True, timeout=60)
|
||||
output = (out.stdout + out.stderr).strip() or "(empty)"
|
||||
except Exception as e:
|
||||
output = f"Error: {{e}}"
|
||||
results.append({{"type": "tool_result", "tool_use_id": b.id, "content": output[:50000]}})
|
||||
history.append({{"role": "user", "content": results}})
|
||||
|
||||
if __name__ == "__main__":
|
||||
h = []
|
||||
print("{name} - Level 0 Agent\\nType 'q' to quit.\\n")
|
||||
while (q := input(">> ").strip()) not in ("q", "quit", ""):
|
||||
print(run(q, h), "\\n")
|
||||
''',
|
||||
|
||||
1: '''#!/usr/bin/env python3
|
||||
"""
|
||||
Level 1 Agent - Model as Agent (~200 lines)
|
||||
|
||||
Core insight: 4 tools cover 90% of coding tasks.
|
||||
The model IS the agent. Code just runs the loop.
|
||||
"""
|
||||
|
||||
from anthropic import Anthropic
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
load_dotenv()
|
||||
|
||||
client = Anthropic(
|
||||
api_key=os.getenv("ANTHROPIC_API_KEY"),
|
||||
base_url=os.getenv("ANTHROPIC_BASE_URL")
|
||||
)
|
||||
MODEL = os.getenv("MODEL_NAME", "claude-sonnet-4-20250514")
|
||||
WORKDIR = Path.cwd()
|
||||
|
||||
SYSTEM = f"""You are a coding agent at {{WORKDIR}}.
|
||||
|
||||
Rules:
|
||||
- Prefer tools over prose. Act, don't just explain.
|
||||
- Never invent file paths. Use ls/find first if unsure.
|
||||
- Make minimal changes. Don't over-engineer.
|
||||
- After finishing, summarize what changed."""
|
||||
|
||||
TOOLS = [
|
||||
{{"name": "bash", "description": "Run shell command",
|
||||
"input_schema": {{"type": "object", "properties": {{"command": {{"type": "string"}}}}, "required": ["command"]}}}},
|
||||
{{"name": "read_file", "description": "Read file contents",
|
||||
"input_schema": {{"type": "object", "properties": {{"path": {{"type": "string"}}}}, "required": ["path"]}}}},
|
||||
{{"name": "write_file", "description": "Write content to file",
|
||||
"input_schema": {{"type": "object", "properties": {{"path": {{"type": "string"}}, "content": {{"type": "string"}}}}, "required": ["path", "content"]}}}},
|
||||
{{"name": "edit_file", "description": "Replace exact text in file",
|
||||
"input_schema": {{"type": "object", "properties": {{"path": {{"type": "string"}}, "old_text": {{"type": "string"}}, "new_text": {{"type": "string"}}}}, "required": ["path", "old_text", "new_text"]}}}},
|
||||
]
|
||||
|
||||
def safe_path(p: str) -> Path:
|
||||
"""Prevent path escape attacks."""
|
||||
path = (WORKDIR / p).resolve()
|
||||
if not path.is_relative_to(WORKDIR):
|
||||
raise ValueError(f"Path escapes workspace: {{p}}")
|
||||
return path
|
||||
|
||||
def execute(name: str, args: dict) -> str:
|
||||
"""Execute a tool and return result."""
|
||||
if name == "bash":
|
||||
dangerous = ["rm -rf /", "sudo", "shutdown", "> /dev/"]
|
||||
if any(d in args["command"] for d in dangerous):
|
||||
return "Error: Dangerous command blocked"
|
||||
try:
|
||||
r = subprocess.run(args["command"], shell=True, cwd=WORKDIR, capture_output=True, text=True, timeout=60)
|
||||
return (r.stdout + r.stderr).strip()[:50000] or "(empty)"
|
||||
except subprocess.TimeoutExpired:
|
||||
return "Error: Timeout (60s)"
|
||||
except Exception as e:
|
||||
return f"Error: {{e}}"
|
||||
|
||||
if name == "read_file":
|
||||
try:
|
||||
return safe_path(args["path"]).read_text()[:50000]
|
||||
except Exception as e:
|
||||
return f"Error: {{e}}"
|
||||
|
||||
if name == "write_file":
|
||||
try:
|
||||
p = safe_path(args["path"])
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(args["content"])
|
||||
return f"Wrote {{len(args['content'])}} bytes to {{args['path']}}"
|
||||
except Exception as e:
|
||||
return f"Error: {{e}}"
|
||||
|
||||
if name == "edit_file":
|
||||
try:
|
||||
p = safe_path(args["path"])
|
||||
content = p.read_text()
|
||||
if args["old_text"] not in content:
|
||||
return f"Error: Text not found in {{args['path']}}"
|
||||
p.write_text(content.replace(args["old_text"], args["new_text"], 1))
|
||||
return f"Edited {{args['path']}}"
|
||||
except Exception as e:
|
||||
return f"Error: {{e}}"
|
||||
|
||||
return f"Unknown tool: {{name}}"
|
||||
|
||||
def agent(prompt: str, history: list = None) -> str:
|
||||
"""Run the agent loop."""
|
||||
if history is None:
|
||||
history = []
|
||||
history.append({{"role": "user", "content": prompt}})
|
||||
|
||||
while True:
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=SYSTEM, messages=history, tools=TOOLS, max_tokens=8000
|
||||
)
|
||||
history.append({{"role": "assistant", "content": response.content}})
|
||||
|
||||
if response.stop_reason != "tool_use":
|
||||
return "".join(b.text for b in response.content if hasattr(b, "text"))
|
||||
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
print(f"> {{block.name}}: {{str(block.input)[:100]}}")
|
||||
output = execute(block.name, block.input)
|
||||
print(f" {{output[:100]}}...")
|
||||
results.append({{"type": "tool_result", "tool_use_id": block.id, "content": output}})
|
||||
history.append({{"role": "user", "content": results}})
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(f"{name} - Level 1 Agent at {{WORKDIR}}")
|
||||
print("Type 'q' to quit.\\n")
|
||||
h = []
|
||||
while True:
|
||||
try:
|
||||
query = input(">> ").strip()
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
break
|
||||
if query in ("q", "quit", "exit", ""):
|
||||
break
|
||||
print(agent(query, h), "\\n")
|
||||
''',
|
||||
}
|
||||
|
||||
ENV_TEMPLATE = '''# API Configuration
|
||||
ANTHROPIC_API_KEY=sk-xxx
|
||||
ANTHROPIC_BASE_URL=https://api.anthropic.com
|
||||
MODEL_NAME=claude-sonnet-4-20250514
|
||||
'''
|
||||
|
||||
|
||||
def create_agent(name: str, level: int, output_dir: Path):
|
||||
"""Create a new agent project."""
|
||||
# Validate level
|
||||
if level not in TEMPLATES and level not in (2, 3, 4):
|
||||
print(f"Error: Level {level} not yet implemented in scaffold.")
|
||||
print("Available levels: 0 (minimal), 1 (4 tools)")
|
||||
print("For levels 2-4, copy from mini-claude-code repository.")
|
||||
sys.exit(1)
|
||||
|
||||
# Create output directory
|
||||
agent_dir = output_dir / name
|
||||
agent_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write agent file
|
||||
agent_file = agent_dir / f"{name}.py"
|
||||
template = TEMPLATES.get(level, TEMPLATES[1])
|
||||
agent_file.write_text(template.format(name=name))
|
||||
print(f"Created: {agent_file}")
|
||||
|
||||
# Write .env.example
|
||||
env_file = agent_dir / ".env.example"
|
||||
env_file.write_text(ENV_TEMPLATE)
|
||||
print(f"Created: {env_file}")
|
||||
|
||||
# Write .gitignore
|
||||
gitignore = agent_dir / ".gitignore"
|
||||
gitignore.write_text(".env\n__pycache__/\n*.pyc\n")
|
||||
print(f"Created: {gitignore}")
|
||||
|
||||
print(f"\nAgent '{name}' created at {agent_dir}")
|
||||
print(f"\nNext steps:")
|
||||
print(f" 1. cd {agent_dir}")
|
||||
print(f" 2. cp .env.example .env")
|
||||
print(f" 3. Edit .env with your API key")
|
||||
print(f" 4. pip install anthropic python-dotenv")
|
||||
print(f" 5. python {name}.py")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Scaffold a new AI coding agent project",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Levels:
|
||||
0 Minimal (~50 lines) - Single bash tool, self-recursion for subagents
|
||||
1 Basic (~200 lines) - 4 core tools: bash, read, write, edit
|
||||
2 Todo (~300 lines) - + TodoWrite for structured planning
|
||||
3 Subagent (~450) - + Task tool for context isolation
|
||||
4 Skills (~550) - + Skill tool for domain expertise
|
||||
"""
|
||||
)
|
||||
parser.add_argument("name", help="Name of the agent to create")
|
||||
parser.add_argument("--level", type=int, default=1, choices=[0, 1, 2, 3, 4],
|
||||
help="Complexity level (default: 1)")
|
||||
parser.add_argument("--path", type=Path, default=Path.cwd(),
|
||||
help="Output directory (default: current directory)")
|
||||
|
||||
args = parser.parse_args()
|
||||
create_agent(args.name, args.level, args.path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,157 @@
|
||||
---
|
||||
name: code-review
|
||||
description: Perform thorough code reviews with security, performance, and maintainability analysis. Use when user asks to review code, check for bugs, or audit a codebase.
|
||||
---
|
||||
|
||||
# Code Review Skill
|
||||
|
||||
You now have expertise in conducting comprehensive code reviews. Follow this structured approach:
|
||||
|
||||
## Review Checklist
|
||||
|
||||
### 1. Security (Critical)
|
||||
|
||||
Check for:
|
||||
- [ ] **Injection vulnerabilities**: SQL, command, XSS, template injection
|
||||
- [ ] **Authentication issues**: Hardcoded credentials, weak auth
|
||||
- [ ] **Authorization flaws**: Missing access controls, IDOR
|
||||
- [ ] **Data exposure**: Sensitive data in logs, error messages
|
||||
- [ ] **Cryptography**: Weak algorithms, improper key management
|
||||
- [ ] **Dependencies**: Known vulnerabilities (check with `npm audit`, `pip-audit`)
|
||||
|
||||
```bash
|
||||
# Quick security scans
|
||||
npm audit # Node.js
|
||||
pip-audit # Python
|
||||
cargo audit # Rust
|
||||
grep -r "password\|secret\|api_key" --include="*.py" --include="*.js"
|
||||
```
|
||||
|
||||
### 2. Correctness
|
||||
|
||||
Check for:
|
||||
- [ ] **Logic errors**: Off-by-one, null handling, edge cases
|
||||
- [ ] **Race conditions**: Concurrent access without synchronization
|
||||
- [ ] **Resource leaks**: Unclosed files, connections, memory
|
||||
- [ ] **Error handling**: Swallowed exceptions, missing error paths
|
||||
- [ ] **Type safety**: Implicit conversions, any types
|
||||
|
||||
### 3. Performance
|
||||
|
||||
Check for:
|
||||
- [ ] **N+1 queries**: Database calls in loops
|
||||
- [ ] **Memory issues**: Large allocations, retained references
|
||||
- [ ] **Blocking operations**: Sync I/O in async code
|
||||
- [ ] **Inefficient algorithms**: O(n^2) when O(n) possible
|
||||
- [ ] **Missing caching**: Repeated expensive computations
|
||||
|
||||
### 4. Maintainability
|
||||
|
||||
Check for:
|
||||
- [ ] **Naming**: Clear, consistent, descriptive
|
||||
- [ ] **Complexity**: Functions > 50 lines, deep nesting > 3 levels
|
||||
- [ ] **Duplication**: Copy-pasted code blocks
|
||||
- [ ] **Dead code**: Unused imports, unreachable branches
|
||||
- [ ] **Comments**: Outdated, redundant, or missing where needed
|
||||
|
||||
### 5. Testing
|
||||
|
||||
Check for:
|
||||
- [ ] **Coverage**: Critical paths tested
|
||||
- [ ] **Edge cases**: Null, empty, boundary values
|
||||
- [ ] **Mocking**: External dependencies isolated
|
||||
- [ ] **Assertions**: Meaningful, specific checks
|
||||
|
||||
## Review Output Format
|
||||
|
||||
```markdown
|
||||
## Code Review: [file/component name]
|
||||
|
||||
### Summary
|
||||
[1-2 sentence overview]
|
||||
|
||||
### Critical Issues
|
||||
1. **[Issue]** (line X): [Description]
|
||||
- Impact: [What could go wrong]
|
||||
- Fix: [Suggested solution]
|
||||
|
||||
### Improvements
|
||||
1. **[Suggestion]** (line X): [Description]
|
||||
|
||||
### Positive Notes
|
||||
- [What was done well]
|
||||
|
||||
### Verdict
|
||||
[ ] Ready to merge
|
||||
[ ] Needs minor changes
|
||||
[ ] Needs major revision
|
||||
```
|
||||
|
||||
## Common Patterns to Flag
|
||||
|
||||
### Python
|
||||
```python
|
||||
# Bad: SQL injection
|
||||
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
|
||||
# Good:
|
||||
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
|
||||
|
||||
# Bad: Command injection
|
||||
os.system(f"ls {user_input}")
|
||||
# Good:
|
||||
subprocess.run(["ls", user_input], check=True)
|
||||
|
||||
# Bad: Mutable default argument
|
||||
def append(item, lst=[]): # Bug: shared mutable default
|
||||
# Good:
|
||||
def append(item, lst=None):
|
||||
lst = lst or []
|
||||
```
|
||||
|
||||
### JavaScript/TypeScript
|
||||
```javascript
|
||||
// Bad: Prototype pollution
|
||||
Object.assign(target, userInput)
|
||||
// Good:
|
||||
Object.assign(target, sanitize(userInput))
|
||||
|
||||
// Bad: eval usage
|
||||
eval(userCode)
|
||||
// Good: Never use eval with user input
|
||||
|
||||
// Bad: Callback hell
|
||||
getData(x => process(x, y => save(y, z => done(z))))
|
||||
// Good:
|
||||
const data = await getData();
|
||||
const processed = await process(data);
|
||||
await save(processed);
|
||||
```
|
||||
|
||||
## Review Commands
|
||||
|
||||
```bash
|
||||
# Show recent changes
|
||||
git diff HEAD~5 --stat
|
||||
git log --oneline -10
|
||||
|
||||
# Find potential issues
|
||||
grep -rn "TODO\|FIXME\|HACK\|XXX" .
|
||||
grep -rn "password\|secret\|token" . --include="*.py"
|
||||
|
||||
# Check complexity (Python)
|
||||
pip install radon && radon cc . -a
|
||||
|
||||
# Check dependencies
|
||||
npm outdated # Node
|
||||
pip list --outdated # Python
|
||||
```
|
||||
|
||||
## Review Workflow
|
||||
|
||||
1. **Understand context**: Read PR description, linked issues
|
||||
2. **Run the code**: Build, test, run locally if possible
|
||||
3. **Read top-down**: Start with main entry points
|
||||
4. **Check tests**: Are changes tested? Do tests pass?
|
||||
5. **Security scan**: Run automated tools
|
||||
6. **Manual review**: Use checklist above
|
||||
7. **Write feedback**: Be specific, suggest fixes, be kind
|
||||
@@ -0,0 +1,213 @@
|
||||
---
|
||||
name: mcp-builder
|
||||
description: Build MCP (Model Context Protocol) servers that give Claude new capabilities. Use when user wants to create an MCP server, add tools to Claude, or integrate external services.
|
||||
---
|
||||
|
||||
# MCP Server Building Skill
|
||||
|
||||
You now have expertise in building MCP (Model Context Protocol) servers. MCP enables Claude to interact with external services through a standardized protocol.
|
||||
|
||||
## What is MCP?
|
||||
|
||||
MCP servers expose:
|
||||
- **Tools**: Functions Claude can call (like API endpoints)
|
||||
- **Resources**: Data Claude can read (like files or database records)
|
||||
- **Prompts**: Pre-built prompt templates
|
||||
|
||||
## Quick Start: Python MCP Server
|
||||
|
||||
### 1. Project Setup
|
||||
|
||||
```bash
|
||||
# Create project
|
||||
mkdir my-mcp-server && cd my-mcp-server
|
||||
python3 -m venv venv && source venv/bin/activate
|
||||
|
||||
# Install MCP SDK
|
||||
pip install mcp
|
||||
```
|
||||
|
||||
### 2. Basic Server Template
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""my_server.py - A simple MCP server"""
|
||||
|
||||
from mcp.server import Server
|
||||
from mcp.server.stdio import stdio_server
|
||||
from mcp.types import Tool, TextContent
|
||||
|
||||
# Create server instance
|
||||
server = Server("my-server")
|
||||
|
||||
# Define a tool
|
||||
@server.tool()
|
||||
async def hello(name: str) -> str:
|
||||
"""Say hello to someone.
|
||||
|
||||
Args:
|
||||
name: The name to greet
|
||||
"""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
@server.tool()
|
||||
async def add_numbers(a: int, b: int) -> str:
|
||||
"""Add two numbers together.
|
||||
|
||||
Args:
|
||||
a: First number
|
||||
b: Second number
|
||||
"""
|
||||
return str(a + b)
|
||||
|
||||
# Run server
|
||||
async def main():
|
||||
async with stdio_server() as (read, write):
|
||||
await server.run(read, write)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### 3. Register with Claude
|
||||
|
||||
Add to `~/.claude/mcp.json`:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-server": {
|
||||
"command": "python3",
|
||||
"args": ["/path/to/my_server.py"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript MCP Server
|
||||
|
||||
### 1. Setup
|
||||
|
||||
```bash
|
||||
mkdir my-mcp-server && cd my-mcp-server
|
||||
npm init -y
|
||||
npm install @modelcontextprotocol/sdk
|
||||
```
|
||||
|
||||
### 2. Template
|
||||
|
||||
```typescript
|
||||
// src/index.ts
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
|
||||
const server = new Server({
|
||||
name: "my-server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
// Define tools
|
||||
server.setRequestHandler("tools/list", async () => ({
|
||||
tools: [
|
||||
{
|
||||
name: "hello",
|
||||
description: "Say hello to someone",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Name to greet" },
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
server.setRequestHandler("tools/call", async (request) => {
|
||||
if (request.params.name === "hello") {
|
||||
const name = request.params.arguments.name;
|
||||
return { content: [{ type: "text", text: `Hello, ${name}!` }] };
|
||||
}
|
||||
throw new Error("Unknown tool");
|
||||
});
|
||||
|
||||
// Start server
|
||||
const transport = new StdioServerTransport();
|
||||
server.connect(transport);
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### External API Integration
|
||||
|
||||
```python
|
||||
import httpx
|
||||
from mcp.server import Server
|
||||
|
||||
server = Server("weather-server")
|
||||
|
||||
@server.tool()
|
||||
async def get_weather(city: str) -> str:
|
||||
"""Get current weather for a city."""
|
||||
async with httpx.AsyncClient() as client:
|
||||
resp = await client.get(
|
||||
f"https://api.weatherapi.com/v1/current.json",
|
||||
params={"key": "YOUR_API_KEY", "q": city}
|
||||
)
|
||||
data = resp.json()
|
||||
return f"{city}: {data['current']['temp_c']}C, {data['current']['condition']['text']}"
|
||||
```
|
||||
|
||||
### Database Access
|
||||
|
||||
```python
|
||||
import sqlite3
|
||||
from mcp.server import Server
|
||||
|
||||
server = Server("db-server")
|
||||
|
||||
@server.tool()
|
||||
async def query_db(sql: str) -> str:
|
||||
"""Execute a read-only SQL query."""
|
||||
if not sql.strip().upper().startswith("SELECT"):
|
||||
return "Error: Only SELECT queries allowed"
|
||||
|
||||
conn = sqlite3.connect("data.db")
|
||||
cursor = conn.execute(sql)
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
return str(rows)
|
||||
```
|
||||
|
||||
### Resources (Read-only Data)
|
||||
|
||||
```python
|
||||
@server.resource("config://settings")
|
||||
async def get_settings() -> str:
|
||||
"""Application settings."""
|
||||
return open("settings.json").read()
|
||||
|
||||
@server.resource("file://{path}")
|
||||
async def read_file(path: str) -> str:
|
||||
"""Read a file from the workspace."""
|
||||
return open(path).read()
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Test with MCP Inspector
|
||||
npx @anthropics/mcp-inspector python3 my_server.py
|
||||
|
||||
# Or send test messages directly
|
||||
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | python3 my_server.py
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Clear tool descriptions**: Claude uses these to decide when to call tools
|
||||
2. **Input validation**: Always validate and sanitize inputs
|
||||
3. **Error handling**: Return meaningful error messages
|
||||
4. **Async by default**: Use async/await for I/O operations
|
||||
5. **Security**: Never expose sensitive operations without auth
|
||||
6. **Idempotency**: Tools should be safe to retry
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
name: pdf
|
||||
description: Process PDF files - extract text, create PDFs, merge documents. Use when user asks to read PDF, create PDF, or work with PDF files.
|
||||
---
|
||||
|
||||
# PDF Processing Skill
|
||||
|
||||
You now have expertise in PDF manipulation. Follow these workflows:
|
||||
|
||||
## Reading PDFs
|
||||
|
||||
**Option 1: Quick text extraction (preferred)**
|
||||
```bash
|
||||
# Using pdftotext (poppler-utils)
|
||||
pdftotext input.pdf - # Output to stdout
|
||||
pdftotext input.pdf output.txt # Output to file
|
||||
|
||||
# If pdftotext not available, try:
|
||||
python3 -c "
|
||||
import fitz # PyMuPDF
|
||||
doc = fitz.open('input.pdf')
|
||||
for page in doc:
|
||||
print(page.get_text())
|
||||
"
|
||||
```
|
||||
|
||||
**Option 2: Page-by-page with metadata**
|
||||
```python
|
||||
import fitz # pip install pymupdf
|
||||
|
||||
doc = fitz.open("input.pdf")
|
||||
print(f"Pages: {len(doc)}")
|
||||
print(f"Metadata: {doc.metadata}")
|
||||
|
||||
for i, page in enumerate(doc):
|
||||
text = page.get_text()
|
||||
print(f"--- Page {i+1} ---")
|
||||
print(text)
|
||||
```
|
||||
|
||||
## Creating PDFs
|
||||
|
||||
**Option 1: From Markdown (recommended)**
|
||||
```bash
|
||||
# Using pandoc
|
||||
pandoc input.md -o output.pdf
|
||||
|
||||
# With custom styling
|
||||
pandoc input.md -o output.pdf --pdf-engine=xelatex -V geometry:margin=1in
|
||||
```
|
||||
|
||||
**Option 2: Programmatically**
|
||||
```python
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
c = canvas.Canvas("output.pdf", pagesize=letter)
|
||||
c.drawString(100, 750, "Hello, PDF!")
|
||||
c.save()
|
||||
```
|
||||
|
||||
**Option 3: From HTML**
|
||||
```bash
|
||||
# Using wkhtmltopdf
|
||||
wkhtmltopdf input.html output.pdf
|
||||
|
||||
# Or with Python
|
||||
python3 -c "
|
||||
import pdfkit
|
||||
pdfkit.from_file('input.html', 'output.pdf')
|
||||
"
|
||||
```
|
||||
|
||||
## Merging PDFs
|
||||
|
||||
```python
|
||||
import fitz
|
||||
|
||||
result = fitz.open()
|
||||
for pdf_path in ["file1.pdf", "file2.pdf", "file3.pdf"]:
|
||||
doc = fitz.open(pdf_path)
|
||||
result.insert_pdf(doc)
|
||||
result.save("merged.pdf")
|
||||
```
|
||||
|
||||
## Splitting PDFs
|
||||
|
||||
```python
|
||||
import fitz
|
||||
|
||||
doc = fitz.open("input.pdf")
|
||||
for i in range(len(doc)):
|
||||
single = fitz.open()
|
||||
single.insert_pdf(doc, from_page=i, to_page=i)
|
||||
single.save(f"page_{i+1}.pdf")
|
||||
```
|
||||
|
||||
## Key Libraries
|
||||
|
||||
| Task | Library | Install |
|
||||
|------|---------|---------|
|
||||
| Read/Write/Merge | PyMuPDF | `pip install pymupdf` |
|
||||
| Create from scratch | ReportLab | `pip install reportlab` |
|
||||
| HTML to PDF | pdfkit | `pip install pdfkit` + wkhtmltopdf |
|
||||
| Text extraction | pdftotext | `brew install poppler` / `apt install poppler-utils` |
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always check if tools are installed** before using them
|
||||
2. **Handle encoding issues** - PDFs may contain various character encodings
|
||||
3. **Large PDFs**: Process page by page to avoid memory issues
|
||||
4. **OCR for scanned PDFs**: Use `pytesseract` if text extraction returns empty
|
||||
Reference in New Issue
Block a user