AutoAgent: Teaching AI to Write Better AI by Letting It Rewrite Itself
Hook
What if instead of asking GPT-4 to help you write an AI agent, you asked it to spend the night rewriting that agent until it passes your benchmarks? That's AutoAgent's premise: humans write instructions, AI writes code.
Context
Building effective AI agents is iterative and tedious. You write prompts, add tools, adjust routing logic, run tests, examine failures, then repeat. Each cycle burns developer time on boilerplate changes—tweaking system messages, reordering tool descriptions, modifying JSON schemas. It's low-leverage work that's perfect for automation, yet most agent frameworks like LangChain and LangGraph still require manual coding for every adjustment.
AutoAgent tackles this by inverting the development model entirely. Instead of using AI to assist human programmers, it uses a meta-agent (a coding LLM) to do the programming while humans provide high-level steering through a markdown instruction file. The meta-agent reads your goals, modifies the agent code, runs benchmarks in isolated Docker containers, and keeps or discards changes based on numeric performance scores. It's hill-climbing optimization applied not to model weights, but to the agent architecture itself—prompts, tools, routing strategies, and configuration. The result is a system that can run overnight, exploring hundreds of agent variations while you sleep.
Technical Insight
At its core, AutoAgent is built around a deceptively simple loop: read instructions from program.md, modify agent.py, run Harbor framework benchmarks, evaluate scores, keep improvements. But the elegance lies in how it constrains the problem space to make autonomous modification tractable.
The agent harness is a single Python file split into two sections. The editable section contains everything the meta-agent can modify—system prompts, tool definitions, routing logic, and configuration parameters. The fixed adapter section handles Harbor integration and shouldn't change. This split gives the meta-agent clear boundaries while maintaining enough structure to prevent it from generating nonsense.
Here's what a minimal editable section looks like:
# EDITABLE SECTION - Meta-agent can modify below this line
SYSTEM_PROMPT = """
You are a helpful assistant that solves tasks step-by-step.
You have access to tools. Use them when appropriate.
"""
# Tool registry - meta-agent can add/remove/modify tools
TOOLS = {
"search": {
"description": "Search the web for current information",
"parameters": {"query": "string"},
"implementation": lambda query: web_search(query)
},
"calculate": {
"description": "Perform mathematical calculations",
"parameters": {"expression": "string"},
"implementation": lambda expr: eval(expr)
}
}
# Routing logic - determines tool selection strategy
def route_action(task_state, llm_response):
if "search" in llm_response.lower():
return "search"
elif any(op in llm_response for op in ['+', '-', '*', '/']):
return "calculate"
return None
CONFIG = {
"model": "gpt-4",
"temperature": 0.7,
"max_iterations": 10
}
The meta-agent reads program.md, which might contain instructions like "Add a tool for reading files" or "Make the system prompt more concise" or "Improve performance on math benchmarks by adjusting the routing logic." It then generates a modified version of agent.py, keeping the adapter section intact while rewriting the editable portion.
Benchmark evaluation uses the Harbor framework, which provides standardized tasks with clear success criteria. Each task lives in its own directory with an instruction.md file, test scripts, and a Dockerfile for isolation. Harbor runs the agent against these tasks and returns a score between 0.0 (complete failure) and 1.0 (perfect success). The meta-agent compares the new score against the previous baseline using simple hill-climbing logic:
def evaluate_and_decide(old_agent, new_agent, benchmark_suite):
old_score = run_benchmarks(old_agent, benchmark_suite)
new_score = run_benchmarks(new_agent, benchmark_suite)
if new_score > old_score:
print(f"Improvement: {old_score} -> {new_score}. Keeping changes.")
return new_agent
else:
print(f"Regression: {old_score} -> {new_score}. Discarding changes.")
return old_agent
No sophisticated optimization algorithms, no gradient descent, no evolutionary selection. Just: if better, keep; if worse, discard. This simplicity is both a strength and a limitation.
The registry-based architecture deserves special attention. By organizing tools and routing logic in dictionaries rather than scattered functions, AutoAgent makes it easier for the meta-agent to reason about structure. Adding a tool means inserting a dictionary entry, not understanding complex Python imports or class hierarchies. This lowers the cognitive load on the meta-agent while maintaining enough structure to prevent chaos.
Docker isolation ensures each benchmark run is reproducible and safe. The meta-agent might generate code that makes expensive API calls, enters infinite loops, or attempts filesystem operations. Running in containers with resource limits and network policies prevents runaway costs and security issues. Each task gets a fresh environment, eliminating state pollution between runs.
Gotcha
Hill-climbing optimization sounds elegant until you hit a local optimum. Imagine your agent scores 0.6 on benchmarks, and every small change makes it worse. The meta-agent discards all modifications and gets stuck, unable to explore the valley that might lead to a 0.9 plateau on the other side. AutoAgent has no built-in mechanisms for escaping local optima—no simulated annealing to accept occasional downgrades, no random restarts to explore different regions of the search space, no population-based methods to maintain diversity. You're relying on the meta-agent's creativity to generate sufficiently varied modifications, which works until it doesn't.
The single-file constraint is both brilliant and limiting. It simplifies the search space beautifully for simple agents, but what happens when you need a 20-tool agent with complex multi-step workflows? That file gets unwieldy fast. There's no clear path to scale AutoAgent to multi-file codebases, shared utility modules, or sophisticated agent architectures. The framework excels at exploring variations within a constrained design space but won't help you discover entirely new architectural patterns that require restructuring beyond one file.
Cost control is conspicuously absent. Running autonomous optimization overnight means potentially thousands of LLM calls—both for the meta-agent generating modifications and for the agent being tested. Without built-in budgets, rate limiting, or cost tracking, you could wake up to a surprise API bill. The framework trusts you to monitor usage externally, which is reasonable for a research tool but concerning for anyone treating it as production infrastructure.
Verdict
Use AutoAgent if you're exploring agent architectures for well-defined benchmarks and have both compute budget and tolerance for experimentation. It shines in research contexts, agent competitions, and rapid prototyping where you can articulate success numerically and want to explore prompt variations, tool combinations, and routing strategies faster than manual iteration allows. It's perfect for the overnight optimization use case: set goals in program.md before bed, wake up to a better agent. Skip it if you're building production systems requiring human oversight, working without clear evaluation metrics, need optimization beyond basic hill-climbing, or have cost constraints that make thousands of experimental LLM calls problematic. Also skip if your agent architecture is too complex for a single file or if you need reproducibility guarantees and audit trails that autonomous modification makes difficult.