| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- #!/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()
|