You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The outer loop evolves Factory workflow graphs (DAGs of agents) against benchmarks using evolutionary search. A workflow graph defines which agents run, in what order, with what prompts. The goal: automatically discover workflow topologies that solve coding tasks better than hand-designed ones.
Current Pipeline
┌─────────────────────────────────────────────────────┐
│ SwarmEngine.run() │
│ │
│ 1. Seed population (4 candidates): │
│ - Slot 0: seed workflow (unchanged) │
│ - Slot 1: random mutation of seed │
│ - Slot 2: minimal design (3 nodes) │
│ - Slot 3: thorough design (10 nodes) │
│ │
│ 2. For each generation: │
│ ┌───────────────────────────────────────┐ │
│ │ For each candidate (SEQUENTIAL): │ │
│ │ For each training instance: │ │
│ │ 1. Extract testbed from Docker │ │
│ │ 2. Run agents on host │ │
│ │ 3. Copy changes into Docker │ │
│ │ 4. Run pytest → pass/fail │ │
│ │ Score = resolved / total │ │
│ └───────────────────────────────────────┘ │
│ Select parents (tournament, k=3) │
│ Mutate parents → offspring │
│ Evaluate offspring (same sequential loop) │
│ Update MAP-Elites archive │
│ │
│ 3. After all generations: │
│ Run holdout audit on best workflow (ONCE) │
│ Export best workflow │
└─────────────────────────────────────────────────────┘
How a Single Evaluation Works
┌──────────────────────────────────────────────────┐
│ DirectFeatureBenchEvaluator._eval_instance() │
│ │
│ Docker Image ──extract──→ /tmp/testbed/ │
│ (pydantic repo at specific commit, │
│ implementation scrambled by setup_patch) │
│ │
│ For each AgentNode in topological order: │
│ factory agent <role> --task "<prompt>" │
│ --project /tmp/testbed/ --timeout N │
│ (runs Claude on the HOST, modifies testbed) │
│ │
│ Restore test files (reverse test_patch.diff) │
│ │
│ docker create <image> │
│ docker cp testbed changes → container │
│ docker exec: pip install -e . && pytest │
│ Exit code 0 → resolved=True │
└──────────────────────────────────────────────────┘
Seed Workflow
researcher ──→ builder ──→ health_checker ──→ gate
(300s) (7200s) (600s)
researcher: "Study the codebase and task. Read /tmp/task-instruction.md.
Explore the repository structure. Write findings."
builder: "Read task description. Read study output. Implement the
feature. Run tests. Fix failures. Commit."
health_checker: "Run test suite. Report results."
gate: "Check if any code was committed."
First Experiment Results
Config: 3 training instances (pydantic lv1), 2 holdout, population 4, budget 20, 4 generations, ~5.5 hours.
All Evaluations
Eval
Gen
Nodes
Type
Inst 1
Inst 2
Inst 3
Train Score
0
0
4
Seed (R→B→HC→G)
✅
✅
✅
1.00
1
0
1
Mutation (1 node)
❌
❌
—
0.00
2
0
3
Designer minimal
❌
❌
❌
0.00
3
0
10
Designer thorough
❌
❌
❌
0.00
4
0
3
Offspring of seed
✅
✅
✅
1.00
5
0
6
Offspring of seed
✅
✅
✅
1.00
6
2
5
Offspring
—
✅
✅
0.67
7
3
3
Holdout audit
✅
❌
—
0.50
Holdout score: 0.485. Training score: 0.785. Overfit delta: 38%. FAILED.
Resource Usage
36 total agent invocations (17 researcher, 13 builder, 6 health_checker)
20 evaluation budget consumed
~5.5 hours wall clock
8 full candidate evaluations completed
Critical Analysis
Problem 1: No Swarm — Everything Is Sequential
The plan says "swarm of parallel experiments." The implementation has evaluate_batch(parallelism=1) with a docstring that says "Currently sequential; parallelism is reserved." Every candidate is evaluated one after another. 4 candidates × 3 instances × ~10 min = 2 hours per generation. This makes evolutionary search impractical — real evolution needs parallel evaluation.
Problem 2: Holdout Only Runs Once at the End
The OverfitDetector.audit() is called exactly once, after all generations complete. During evolution, every candidate is scored only on training instances. There is no per-generation holdout tracking. This means:
We have no idea if candidates generalize during evolution
A workflow could overfit for 3 generations straight and we'd only discover it at the end
The holdout score should be computed every generation for the best candidate and tracked in GenerationSummary. Holdout is the metric we report — it's the only way to know if evolution is producing real improvement vs memorizing the training set.
Problem 3: Training Set Was Too Easy — No Evolutionary Signal
The seed scored 1.0 on all 3 pydantic lv1 training problems in generation 0. Evolution had no signal to optimize on — you can't beat 100%.
Two separate problems here:
Too few instances. 3 is not enough for a meaningful score. Need 10+ to get a stable signal.
Too easy. The seed (4-node workflow with researcher+builder+health_checker) solved everything. The seed should be SIMPLE — just a bare builder with no helpers — so evolution can discover that adding a researcher or QA agent actually helps.
Problem 4: Researcher Agent Has Web Search — Answer Leakage
The researcher agent gets WebSearch and WebFetch tools. FeatureBench tasks come from real open-source repos (pydantic, fastapi, etc.). The solutions exist on GitHub, StackOverflow, and blog posts. The researcher can literally google the answer.
The FeatureBench instruction says "You are forbidden to access https://github.com/pydantic/pydantic" but this is text in instruction.md, not an enforced tool constraint. The agent can still find solutions through indirect search results.
Problem 5: Designer Workflows Don't Work
The DesignerAgent.design_minimal() and design_thorough() create workflows with generic prompts that don't reference /tmp/task-instruction.md. The seed works because its prompts are carefully tuned to the FeatureBench environment. The designer workflows are structurally diverse but functionally broken.
All 3 designer/mutation variants scored 0.0 in generation 0. The "diversity" is fake — these candidates can't even read the task description.
Problem 6: Mutations Can Only Simplify, Not Improve
The 6 mutation operators modify graph structure (add/remove nodes, change edges) but don't touch prompts. Since a workflow's effectiveness is ~90% determined by its prompts:
Removing a node might work (if it was unnecessary) or break the workflow
Inserting a node adds a do-nothing agent (no prompt = no value)
The only productive mutations are removing unnecessary nodes
The evolution is just pruning, not improving. The best workflow (3 nodes) is the seed with the health_checker removed — a simplification, not an innovation.
Problem 7: Generation Lifecycle Is Messy
Generation 0 had 6 evaluations (4 initial + 2 offspring). Generation 1 appears empty (starts and immediately transitions to generation 2 at the same timestamp). Generation 2 had 1 evaluation. Generation 3 had 1 evaluation (holdout). The budget didn't drain cleanly across generations.
But hygiene_score is always 0.0 (never computed) and cost_usd is always 0.0 (direct evaluator doesn't track tokens). The multi-metric protection against Goodhart's Law is non-functional. The actual signal is just 0.6 * benchmark + 0.185.
Problem 9: No Per-Instance Tracking
We don't store which instances each candidate solved across generations. This means we can't do instance-level failure analysis, consistency checks, or smart subset rotation.
What Works
Despite the problems, these components are solid and reusable:
MAP-Elites archive with 4D feature grid
Novelty filter (hash + graph edit distance)
6 structural mutation operators with validation and frozen node protection
Add --disallowedTools WebSearch,WebFetch to agent invocations in DirectFeatureBenchEvaluator._run_workflow_agents(). Simplest fix, biggest integrity impact. Already partially implemented (commit eb97ec84).
Fix 2: Simplify fitness to raw pass rate
Remove the broken composite formula. Score = resolved / total. Nothing else. Training score drives parent selection within a generation. Holdout score is the reported result. No hygiene, no cost normalization, no complexity penalty — these were all zeros anyway.
Training score is useful signal for directing the search (which mutations are working, which parents to select from). It's not noise. But holdout score is what we report as the outcome — it's the only way to know if the evolved workflow actually generalizes.
Fix 3: Holdout every generation
Move holdout evaluation into evolve_generation(). After evaluating the population on training instances, evaluate the best candidate on holdout. Track holdout_score in GenerationSummary. This gives per-generation visibility into generalization. Already partially implemented (commit eb97ec84).
Important: holdout score is for REPORTING only. Do NOT use it for parent selection — that would let evolution overfit to holdout too, just more slowly. Training drives search, holdout measures generalization.
Run the SIMPLEST possible workflow (just a bare builder, 1 node, no researcher) on 20-30 FeatureBench problems
Pick 10 where the simple builder scores 0.3-0.7 (partial success) — these are problems where a better workflow could help
Pick 3-5 additional problems at similar difficulty for holdout
Discard problems where the builder scores 0.0 (too hard) or 1.0 (too easy)
The seed for evolution should be this simple builder (1 node). Evolution should discover that adding a researcher, code reviewer, or QA agent improves the score. Starting with a 4-node seed that already aces everything leaves no room for improvement.
Fix 5: Fix designer prompts
All workflows (designed, mutated, or seeded) must include the critical benchmark-specific prompt content: "Read /tmp/task-instruction.md", implementation rules, commit rules. The topology can vary but the essential instructions must be present. Otherwise designed workflows are structurally diverse but functionally dead.
Add PROMPT_MUTATE to the mutation type enum. When selected:
Pick a random AgentNode in the workflow
The Designer Agent reads: the current prompt, the benchmark spec, and failure telemetry from the last evaluation
The Designer Agent proposes a revised prompt (complete replacement, not a diff)
This is a single LLM call per mutation
This gives evolution two levers: topology (which agents, what order) AND prompts (what instructions each agent gets). Without this, evolution can only prune nodes — it can't make agents better at their jobs.
This is a simple version, not full bi-level optimization. The full version (outer loop evolves topology, inner SkillOpt loop optimizes prompts within each topology) is v2. For now, prompt mutations are just another mutation operator alongside the structural ones.
Fix 7: Parallel evaluation
Use concurrent.futures.ProcessPoolExecutor in evaluate_batch(). Each candidate gets its own temp directory and Docker container. Target: 4 concurrent evaluations on the remote-claude machine (12 CPU, 64GB RAM via Colima).
Fix 8: Clean generation lifecycle
Fixed structure per generation:
Evaluate current population on training instances
Evaluate best on holdout instances (for reporting)
Select parents from archive (using training scores)
Generate offspring via mutation
Log GenerationSummary with both training and holdout scores
No skipping, no merging, no variable sizes.
Fix 9: Per-instance tracking
Store instance-level results (which instances each candidate solved/failed) in the archive. Enables failure analysis and consistency checks.
The insert_node mutation operator needs to create AgentNodes with functional prompts that include the benchmark-specific patterns (task file path, implementation rules), not empty placeholders. If the operator just inserts a generic node with no prompt, the mutation is structurally valid but functionally useless.
Learnings
Training score is useful signal, holdout is the only reported result. Training drives the search — which mutations work, which parents to select. Holdout measures whether the result generalizes. Both matter, for different purposes.
Topology-only evolution without prompt evolution is pruning, not improving. The 10-node workflow scored 0 because its prompts were wrong, not because 10 nodes is a bad topology.
Web search = answer leakage on benchmark tasks. Must be blocked for fair evaluation.
Calibration before evolution is mandatory. Start with a simple seed (bare builder). Use problems where the simple seed scores 0.3-0.7 so evolution has gradient to climb.
Sequential evaluation makes evolution impractical. A real run takes hours. Parallelism is not optional.
Designer-created workflows must be functional, not just diverse. Structural diversity without working prompts is zero signal.
Outer Loop v1: Post-Mortem and Fix Plan
What We Built
The outer loop evolves Factory workflow graphs (DAGs of agents) against benchmarks using evolutionary search. A workflow graph defines which agents run, in what order, with what prompts. The goal: automatically discover workflow topologies that solve coding tasks better than hand-designed ones.
Current Pipeline
How a Single Evaluation Works
Seed Workflow
First Experiment Results
Config: 3 training instances (pydantic lv1), 2 holdout, population 4, budget 20, 4 generations, ~5.5 hours.
All Evaluations
Holdout score: 0.485. Training score: 0.785. Overfit delta: 38%. FAILED.
Resource Usage
Critical Analysis
Problem 1: No Swarm — Everything Is Sequential
The plan says "swarm of parallel experiments." The implementation has
evaluate_batch(parallelism=1)with a docstring that says "Currently sequential; parallelism is reserved." Every candidate is evaluated one after another. 4 candidates × 3 instances × ~10 min = 2 hours per generation. This makes evolutionary search impractical — real evolution needs parallel evaluation.Problem 2: Holdout Only Runs Once at the End
The
OverfitDetector.audit()is called exactly once, after all generations complete. During evolution, every candidate is scored only on training instances. There is no per-generation holdout tracking. This means:The holdout score should be computed every generation for the best candidate and tracked in
GenerationSummary. Holdout is the metric we report — it's the only way to know if evolution is producing real improvement vs memorizing the training set.Problem 3: Training Set Was Too Easy — No Evolutionary Signal
The seed scored 1.0 on all 3 pydantic lv1 training problems in generation 0. Evolution had no signal to optimize on — you can't beat 100%.
Two separate problems here:
Problem 4: Researcher Agent Has Web Search — Answer Leakage
The researcher agent gets
WebSearchandWebFetchtools. FeatureBench tasks come from real open-source repos (pydantic, fastapi, etc.). The solutions exist on GitHub, StackOverflow, and blog posts. The researcher can literally google the answer.The FeatureBench instruction says "You are forbidden to access https://github.com/pydantic/pydantic" but this is text in instruction.md, not an enforced tool constraint. The agent can still find solutions through indirect search results.
Problem 5: Designer Workflows Don't Work
The
DesignerAgent.design_minimal()anddesign_thorough()create workflows with generic prompts that don't reference/tmp/task-instruction.md. The seed works because its prompts are carefully tuned to the FeatureBench environment. The designer workflows are structurally diverse but functionally broken.All 3 designer/mutation variants scored 0.0 in generation 0. The "diversity" is fake — these candidates can't even read the task description.
Problem 6: Mutations Can Only Simplify, Not Improve
The 6 mutation operators modify graph structure (add/remove nodes, change edges) but don't touch prompts. Since a workflow's effectiveness is ~90% determined by its prompts:
The evolution is just pruning, not improving. The best workflow (3 nodes) is the seed with the health_checker removed — a simplification, not an innovation.
Problem 7: Generation Lifecycle Is Messy
Generation 0 had 6 evaluations (4 initial + 2 offspring). Generation 1 appears empty (starts and immediately transitions to generation 2 at the same timestamp). Generation 2 had 1 evaluation. Generation 3 had 1 evaluation (holdout). The budget didn't drain cleanly across generations.
Problem 8: Fitness Signal Is Broken
The composite score formula is:
But
hygiene_scoreis always 0.0 (never computed) andcost_usdis always 0.0 (direct evaluator doesn't track tokens). The multi-metric protection against Goodhart's Law is non-functional. The actual signal is just0.6 * benchmark + 0.185.Problem 9: No Per-Instance Tracking
We don't store which instances each candidate solved across generations. This means we can't do instance-level failure analysis, consistency checks, or smart subset rotation.
What Works
Despite the problems, these components are solid and reusable:
Fix Plan (Priority Order)
Fix 1: Block web search in evaluator
Add
--disallowedTools WebSearch,WebFetchto agent invocations inDirectFeatureBenchEvaluator._run_workflow_agents(). Simplest fix, biggest integrity impact. Already partially implemented (commiteb97ec84).Fix 2: Simplify fitness to raw pass rate
Remove the broken composite formula. Score = resolved / total. Nothing else. Training score drives parent selection within a generation. Holdout score is the reported result. No hygiene, no cost normalization, no complexity penalty — these were all zeros anyway.
Training score is useful signal for directing the search (which mutations are working, which parents to select from). It's not noise. But holdout score is what we report as the outcome — it's the only way to know if the evolved workflow actually generalizes.
Fix 3: Holdout every generation
Move holdout evaluation into
evolve_generation(). After evaluating the population on training instances, evaluate the best candidate on holdout. Trackholdout_scoreinGenerationSummary. This gives per-generation visibility into generalization. Already partially implemented (commiteb97ec84).Important: holdout score is for REPORTING only. Do NOT use it for parent selection — that would let evolution overfit to holdout too, just more slowly. Training drives search, holdout measures generalization.
Fix 4: Calibrate difficulty — 10 training, 3-5 holdout
Before running evolution:
The seed for evolution should be this simple builder (1 node). Evolution should discover that adding a researcher, code reviewer, or QA agent improves the score. Starting with a 4-node seed that already aces everything leaves no room for improvement.
Fix 5: Fix designer prompts
All workflows (designed, mutated, or seeded) must include the critical benchmark-specific prompt content: "Read /tmp/task-instruction.md", implementation rules, commit rules. The topology can vary but the essential instructions must be present. Otherwise designed workflows are structurally diverse but functionally dead.
Fix 6: Add prompt mutation operator (simple version)
Add
PROMPT_MUTATEto the mutation type enum. When selected:This gives evolution two levers: topology (which agents, what order) AND prompts (what instructions each agent gets). Without this, evolution can only prune nodes — it can't make agents better at their jobs.
This is a simple version, not full bi-level optimization. The full version (outer loop evolves topology, inner SkillOpt loop optimizes prompts within each topology) is v2. For now, prompt mutations are just another mutation operator alongside the structural ones.
Fix 7: Parallel evaluation
Use
concurrent.futures.ProcessPoolExecutorinevaluate_batch(). Each candidate gets its own temp directory and Docker container. Target: 4 concurrent evaluations on the remote-claude machine (12 CPU, 64GB RAM via Colima).Fix 8: Clean generation lifecycle
Fixed structure per generation:
No skipping, no merging, no variable sizes.
Fix 9: Per-instance tracking
Store instance-level results (which instances each candidate solved/failed) in the archive. Enables failure analysis and consistency checks.
Fix 10: Verify insert_node creates functional agents
The
insert_nodemutation operator needs to create AgentNodes with functional prompts that include the benchmark-specific patterns (task file path, implementation rules), not empty placeholders. If the operator just inserts a generic node with no prompt, the mutation is structurally valid but functionally useless.Learnings
Files
factory/outer_loop/engine.py— SwarmEngine (412 lines)factory/outer_loop/evaluator.py— SwarmEvaluator + FitnessCache (139 lines)factory/outer_loop/direct_evaluator.py— FeatureBench evaluator (441 lines)factory/outer_loop/mutations.py— 6 operators + MutationStrategy (557 lines)factory/outer_loop/population.py— Population + MAPElitesArchive (203 lines)factory/outer_loop/similarity.py— hash, GED, NoveltyFilter (134 lines)factory/outer_loop/overfit.py— OverfitDetector (78 lines)factory/outer_loop/designer.py— DesignerAgent dual mode (344 lines)factory/outer_loop/models.py— Pydantic models (184 lines)factory/outer_loop/run_evolution.py— CLI runner (133 lines)feat/outer-loop-evolution