Prerequisites
This section synthesizes material from the entire chapter: the six-phase research loop and novelty filter from Section 53.1, the architectural lessons from Section 53.2, and the safety architecture from Section 53.3. You will also need familiarity with experiment tracking from Chapter 47, the Claude Code SDK from Chapter 16, and the Discovery Workbench architecture from Chapter 6.
This section is the culmination of the chapter: a complete, buildable recipe for a supervised AI scientist. "Supervised" means Level 2 on the autonomy spectrum from Section 53.1: the system runs the research loop autonomously between human gates, but a human supervisor must approve at three critical junctures (hypothesis approval, experiment approval, and publication approval). This architecture balances the productivity gains of automation with the safety and judgment of human oversight. Each phase runs as a separate agent, orchestrated with LangGraph, tracked with MLflow, and integrated into the Discovery Workbench.
1. Architecture Overview
What happens when you give an LLM a research question, a compute budget, and permission to write its own experiments? It generates a hypothesis, codes a script, runs it in a sandbox, evaluates the results, drafts a report, and then waits for a human to decide whether any of it was worth doing.
Without a structured pipeline, teams that hand an LLM a research question and a compute budget routinely burn through dollars on circular hypotheses, unreproducible code, and self-congratulatory evaluations that no human ever reviews. The architecture below exists to prevent exactly those failure modes.
The supervised AI scientist consists of six agents connected by a LangGraph state machine, where LangGraph is a graph-based agent orchestration framework that models workflows as nodes (agents) and edges (transitions), with built-in support for checkpointing and human-in-the-loop interrupts. Three of the transitions are human gates: points where the system pauses and waits for human approval before proceeding. The architecture follows the separation principle from Section 53.2's lessons: no agent evaluates its own output. Figure 53.4 shows the complete pipeline, with human gates highlighted in gold and feedback loops shown as dashed red arrows. Figure 53.4.1 illustrates Supervised AI scientist pipeline with human gates and feedback loops.
Human Gates Defined
A human gate is a programmatic checkpoint where the pipeline halts and transfers control to a human supervisor. The supervisor must explicitly approve or reject the system's output before execution resumes. Human gates matter because fully autonomous research systems can pursue unproductive directions, introduce subtle methodological errors, or waste compute on low-value experiments. A well-placed gate catches these failures before they propagate. Mechanistically, a human gate serializes the pipeline's current state, presents a summary to the supervisor through a user interface (UI) or command-line interface (CLI) prompt, and blocks the state machine transition until the supervisor provides a binary decision. Use human gates (rather than fully automated quality checks) whenever the decision requires value judgments about research direction, safety implications of running generated code, or whether results merit publication. Use automated checks for mechanical validations such as type checking, schema conformance, or resource limit enforcement.
2. State Definition
All data flows between agents through a single shared LangGraph state object. Each agent reads from and writes to this state, so downstream agents can access everything produced upstream. In short: a single shared state object is the pipeline's memory; every agent reads what came before and writes what comes next, so no discovery is ever lost between phases.
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
class Phase(Enum):
HYPOTHESIS = "hypothesis"
HYPOTHESIS_GATE = "hypothesis_gate"
CODING = "coding"
EXPERIMENT_GATE = "experiment_gate"
RUNNING = "running"
EVALUATION = "evaluation"
REVIEW = "review"
REPORT_GATE = "report_gate"
REPORTING = "reporting"
DONE = "done"
@dataclass
class ResearchState:
"""Shared state for the supervised AI scientist pipeline."""
# Configuration
research_domain: str = ""
max_retries: int = 3
compute_budget_dollars: float = 50.0
# Phase tracking
current_phase: Phase = Phase.HYPOTHESIS
iteration: int = 0
spent_dollars: float = 0.0
# Hypothesis phase
hypothesis: str = ""
hypothesis_rationale: str = ""
novelty_score: float = 0.0
literature_context: list[str] = field(
default_factory=list
)
# Coding phase
experiment_code: str = ""
code_review_notes: str = ""
sandbox_path: str = ""
# Experiment phase
raw_results: dict[str, Any] = field(
default_factory=dict
)
metrics: dict[str, float] = field(
default_factory=dict
)
artifacts: list[str] = field(default_factory=list)
# Evaluation phase
statistical_tests: dict[str, Any] = field(
default_factory=dict
)
is_significant: bool = False
effect_size: float = 0.0
# Review phase
review_verdict: str = "" # "accept", "revise", "reject"
review_comments: list[str] = field(
default_factory=list
)
# Report phase
report_markdown: str = ""
mlflow_run_id: str = ""
# Human gate decisions
gates_passed: dict[str, bool] = field(
default_factory=dict
)
gates_passed dictionary tracks human approval decisions.3. Hypothesis Generation Agent
The hypothesis agent combines gap analysis from Chapter 39 with the novelty filter from Section 53.1. It generates a hypothesis, checks it against existing literature using PaperQA2 (a retrieval-augmented question-answering system that searches and synthesizes scientific papers to ground LLM responses in published literature), and passes it to the first human gate.
import anthropic
import json
class HypothesisAgent:
"""Generate and validate research hypotheses."""
def __init__(
self,
novelty_filter, # NoveltyFilter from Section 53.1
literature_agent, # PaperQA2 wrapper
):
self.client = anthropic.Anthropic()
self.novelty_filter = novelty_filter
self.literature = literature_agent
def run(self, state: ResearchState) -> ResearchState:
"""Generate a novel, literature-grounded hypothesis."""
# Step 1: Gather literature context
context_query = (
f"What are the current open problems and recent "
f"advances in {state.research_domain}?"
)
lit_results = self.literature.search(context_query, k=10)
state.literature_context = [
f"{r.title}: {r.summary}" for r in lit_results
]
# Step 2: Generate hypothesis
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
system=(
"You are a research scientist. Generate a single "
"testable hypothesis based on the provided literature "
"context. The hypothesis must be: (1) specific enough "
"to test with a single experiment, (2) novel relative "
"to the cited literature, (3) feasible within a $50 "
"compute budget. Output JSON with 'hypothesis' and "
"'rationale' fields."
),
messages=[{
"role": "user",
"content": (
f"Research domain: {state.research_domain}\n\n"
f"Literature context:\n"
+ "\n".join(state.literature_context)
),
}],
)
result = json.loads(response.content[0].text)
state.hypothesis = result["hypothesis"]
state.hypothesis_rationale = result["rationale"]
# Step 3: Novelty check
novelty = self.novelty_filter.check_novelty(
state.hypothesis
)
state.novelty_score = novelty["distance"]
if not novelty["is_novel"]:
# Regenerate with explicit instruction to differ
state.iteration += 1
if state.iteration < state.max_retries:
return self.run(state) # retry
state.current_phase = Phase.HYPOTHESIS_GATE
return state
max_retries times if the generated hypothesis is too similar to existing work.4. Coding Agent with Claude Code SDK
The coding agent uses the Claude Code software development kit (SDK) to write experiment scripts in an agentic loop. Unlike direct API calls, the SDK provides the agent with file system access, terminal execution, and iterative debugging within a sandboxed environment. This is the same capability we used for AI-assisted implementation in Chapter 16, now directed at writing scientific experiments rather than production software.
import subprocess
import json
import tempfile
from pathlib import Path
class CodingAgent:
"""Write experiment code using Claude Code SDK."""
def __init__(
self,
sandbox_dir: str | None = None,
max_iterations: int = 5,
):
self.sandbox_dir = sandbox_dir or tempfile.mkdtemp(
prefix="ai_scientist_"
)
self.max_iterations = max_iterations
def run(self, state: ResearchState) -> ResearchState:
"""Generate experiment code for the approved hypothesis."""
prompt = self._build_prompt(state)
# Use Claude Code SDK in subprocess mode
result = subprocess.run(
[
"claude",
"--print",
"--output-format", "json",
"--max-turns", str(self.max_iterations),
"--allowedTools", "Edit,Write,Bash",
"-p", prompt,
],
capture_output=True,
text=True,
cwd=self.sandbox_dir,
timeout=300,
)
response = json.loads(result.stdout)
state.experiment_code = self._read_generated_code()
state.sandbox_path = self.sandbox_dir
state.current_phase = Phase.EXPERIMENT_GATE
return state
def _build_prompt(self, state: ResearchState) -> str:
return f"""You are writing a scientific experiment script.
HYPOTHESIS: {state.hypothesis}
RATIONALE: {state.hypothesis_rationale}
Write a complete Python experiment script (experiment.py) that:
1. Tests the hypothesis with a controlled experiment
2. Includes a baseline comparison
3. Runs 5 random seeds for statistical validity
4. Saves all metrics to results.json
5. Generates at least one visualization (saved as plot.png)
6. Completes within 30 minutes on a single GPU
7. Uses standard libraries (PyTorch, scikit-learn, numpy, matplotlib)
Also write a requirements.txt for any dependencies.
The script must be self-contained and runnable with:
python experiment.py
Save all outputs to a 'results/' subdirectory."""
def _read_generated_code(self) -> str:
code_path = Path(self.sandbox_dir) / "experiment.py"
if code_path.exists():
return code_path.read_text()
return ""
The Claude Code SDK (invoked as a subprocess above) provides three capabilities that
direct API calls do not: (1) file system access, so the agent can read existing code and
write new files; (2) terminal access, so it can install packages and run test commands
during development; (3) iterative debugging, so it can execute the script, see errors,
and fix them in a loop. A direct anthropic.Anthropic().messages.create()
call would produce a code string but could not test it. The SDK transforms code
generation from a single-shot prediction into an iterative development process, which in practice
tends to reduce the rate of code-result mismatches (the failure mode identified in Section 53.2).
The cost overhead is typically around 3x compared to a single API call, but the reliability
improvement generally justifies it.
5. Experiment Runner with Sandboxing
The experiment runner executes the generated code in an isolated environment. Sandboxing is essential: AI-generated code should never run with access to the host file system, network, or other experiments' data. We use Docker containers (lightweight virtualization units that package code and dependencies into an isolated runtime environment) for isolation and MLflow, an open-source platform for tracking experiment parameters, metrics, and artifacts across runs, for tracking.
import subprocess
import json
import mlflow
from pathlib import Path
class ExperimentRunner:
"""Execute experiments in sandboxed containers."""
def __init__(
self,
docker_image: str = "python:3.11-slim",
gpu: bool = True,
timeout_seconds: int = 1800,
):
self.docker_image = docker_image
self.gpu = gpu
self.timeout = timeout_seconds
def run(self, state: ResearchState) -> ResearchState:
"""Run the experiment in a Docker container."""
sandbox = Path(state.sandbox_path)
# Start MLflow run
with mlflow.start_run() as run:
state.mlflow_run_id = run.info.run_id
# Log hypothesis and code as artifacts
mlflow.log_param("hypothesis", state.hypothesis[:250])
mlflow.log_param("novelty_score", state.novelty_score)
mlflow.log_artifact(str(sandbox / "experiment.py"))
# Build Docker command
gpu_flag = "--gpus all" if self.gpu else ""
cmd = (
f"docker run --rm {gpu_flag} "
f"-v {sandbox}:/workspace "
f"-w /workspace "
f"{self.docker_image} "
f"bash -c '"
f"pip install -r requirements.txt "
f"&& python experiment.py'"
)
try:
result = subprocess.run(
cmd,
shell=True,
capture_output=True,
text=True,
timeout=self.timeout,
)
# Read results
results_path = sandbox / "results" / "results.json"
if results_path.exists():
state.raw_results = json.loads(
results_path.read_text()
)
# Log metrics to MLflow
for key, value in state.raw_results.items():
if isinstance(value, (int, float)):
mlflow.log_metric(key, value)
state.metrics[key] = value
# Log artifacts (plots, saved models)
results_dir = sandbox / "results"
if results_dir.exists():
for artifact in results_dir.iterdir():
mlflow.log_artifact(str(artifact))
state.artifacts.append(str(artifact))
state.current_phase = Phase.EVALUATION
except subprocess.TimeoutExpired:
mlflow.log_param("status", "timeout")
state.review_verdict = "reject"
state.review_comments.append(
f"Experiment timed out after "
f"{self.timeout}s."
)
state.current_phase = Phase.REVIEW
except Exception as e:
mlflow.log_param("status", "error")
mlflow.log_param("error", str(e)[:500])
state.current_phase = Phase.CODING # retry
return state
6. Evaluation and Review Agents
The evaluation agent performs statistical analysis on the raw results, testing whether the hypothesis is supported. The review agent then critiques the entire process, checking for methodological flaws. These two agents enforce the separation principle: the system that ran the experiment does not judge its own output.
Mental Model
Think of the separation between experiment execution and evaluation as the relationship between a chef and a restaurant critic. The chef (experiment runner) prepares the dish following a recipe, tasting along the way to get the seasoning right. But the published review comes from the critic (evaluation and review agents), who has no stake in the kitchen's reputation. If the chef also wrote the review, every dish would be "exquisite." The same applies here: if the agent that generated the hypothesis and code also judged whether the results were significant, it would be structurally biased toward confirming its own work. Splitting generation from judgment forces the evaluation to operate on outputs alone, without access to the optimism that produced them.
from scipy import stats
import numpy as np
class EvaluationAgent:
"""Statistical evaluation of experiment results."""
def run(self, state: ResearchState) -> ResearchState:
"""Analyze results for statistical significance."""
results = state.raw_results
# Extract baseline and treatment results
baseline = np.array(
results.get("baseline_scores", [])
)
treatment = np.array(
results.get("treatment_scores", [])
)
if len(baseline) < 2 or len(treatment) < 2:
state.is_significant = False
state.statistical_tests["error"] = (
"Insufficient data points for testing"
)
state.current_phase = Phase.REVIEW
return state
# Paired t-test (if same seeds used)
if len(baseline) == len(treatment):
t_stat, p_value = stats.ttest_rel(
treatment, baseline
)
else:
t_stat, p_value = stats.ttest_ind(
treatment, baseline
)
# Effect size (Cohen's d: standardized difference
# between two means, where d > 0.8 is "large")
pooled_std = np.sqrt(
(np.std(baseline) ** 2 + np.std(treatment) ** 2)
/ 2
)
cohens_d = (
(np.mean(treatment) - np.mean(baseline))
/ pooled_std
if pooled_std > 0
else 0.0
)
state.statistical_tests = {
"test": "paired_t_test",
"t_statistic": float(t_stat),
"p_value": float(p_value),
"cohens_d": float(cohens_d),
"baseline_mean": float(np.mean(baseline)),
"treatment_mean": float(np.mean(treatment)),
"n_seeds": len(baseline),
}
state.is_significant = p_value < 0.05
state.effect_size = abs(cohens_d)
state.current_phase = Phase.REVIEW
return state
class ReviewAgent:
"""Methodological review of the experiment."""
def __init__(self):
self.client = anthropic.Anthropic()
def run(self, state: ResearchState) -> ResearchState:
"""Critique the experiment methodology and results."""
review_prompt = f"""You are a rigorous scientific reviewer.
Review the following experiment for methodological flaws.
HYPOTHESIS: {state.hypothesis}
EXPERIMENT CODE (first 2000 chars):
{state.experiment_code[:2000]}
RESULTS:
{json.dumps(state.statistical_tests, indent=2)}
Check for:
1. Data leakage between train and test sets
2. Proper random seed control
3. Fair baseline comparison (same hyperparameter budget)
4. Appropriate statistical test for the data
5. Multiple comparisons correction if needed
6. Effect size interpretation (is it practically meaningful?)
7. Code bugs that could invalidate results
Output JSON with:
- "verdict": "accept", "revise", or "reject"
- "comments": list of specific issues found
- "severity": "minor", "major", or "critical" for each comment
"""
response = self.client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=2048,
messages=[{
"role": "user", "content": review_prompt,
}],
)
review = json.loads(response.content[0].text)
state.review_verdict = review["verdict"]
state.review_comments = review["comments"]
if review["verdict"] == "revise":
state.iteration += 1
if state.iteration < state.max_retries:
state.current_phase = Phase.CODING
else:
state.review_verdict = "reject"
state.current_phase = Phase.REPORT_GATE
elif review["verdict"] == "reject":
state.current_phase = Phase.HYPOTHESIS
else:
state.current_phase = Phase.REPORT_GATE
return state
Exercise 53.4.1
The ReviewAgent in Listing 53.12 routes to one of three destinations based on
its verdict: "accept" sends the pipeline to the report gate,
"revise" sends it back to the coding agent, and "reject" restarts
from hypothesis generation. Suppose the max_retries is set to 3 and the review
agent returns "revise" four times in a row. Trace through the
ReviewAgent.run method to determine: (a) which phase the pipeline enters after
the fourth "revise" verdict, and (b) what the value of review_verdict is at
that point. Then explain why the pipeline does not loop forever even without human
intervention at the gates.
Hint
Look at lines where state.iteration is compared to state.max_retries.
On iterations 1, 2, and 3 the verdict stays "revise" and the phase resets to
CODING. On iteration 4, the condition state.iteration < state.max_retries
is false (4 is not less than 3), so the method takes the else branch. Check what that
branch sets for both review_verdict and current_phase.
Common Misconception
A frequent misconception is that separating the generation and evaluation agents into different LLM calls eliminates evaluation bias. It reduces bias, but it does not eliminate it: both agents share the same underlying model weights, training data, and systematic blind spots. If the base model has a tendency to overvalue certain experimental designs or underweight specific statistical pitfalls, that tendency persists regardless of which "role" the model is playing. True independence requires incorporating non-large language model (LLM) evaluation components (deterministic statistical tests, formal verification, domain-specific unit tests) alongside the LLM-based review, which is why the pipeline includes the scipy-based EvaluationAgent as a complement to the LLM-based ReviewAgent.
7. Orchestrating with LangGraph
Wiring the agents together requires a mechanism for forward data flow, backward feedback loops, and execution halts at human gates.
LangGraph connects the agents into a state machine with conditional edges and human-in-the-loop
gates. The interrupt_before parameter pauses execution at each human gate
and presents the current state to the supervisor for approval.
(As of 2025, LangGraph's API has evolved. Newer versions use langgraph.checkpoint.memory.InMemorySaver rather than MemorySaver, and the interrupt function replaces raw interrupt_before lists for finer-grained control. The architectural pattern shown here remains valid; consult the current LangGraph documentation for updated import paths.)
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
def build_ai_scientist_graph(
hypothesis_agent: HypothesisAgent,
coding_agent: CodingAgent,
runner: ExperimentRunner,
evaluation_agent: EvaluationAgent,
review_agent: ReviewAgent,
) -> StateGraph:
"""Build the supervised AI scientist state graph."""
graph = StateGraph(ResearchState)
# Add agent nodes
graph.add_node("hypothesis", hypothesis_agent.run)
graph.add_node("coding", coding_agent.run)
graph.add_node("experiment", runner.run)
graph.add_node("evaluation", evaluation_agent.run)
graph.add_node("review", review_agent.run)
graph.add_node("report", generate_report)
# Human gate nodes (identity functions; the interrupt
# happens before the node executes)
graph.add_node(
"hypothesis_gate", lambda s: s
)
graph.add_node(
"experiment_gate", lambda s: s
)
graph.add_node(
"report_gate", lambda s: s
)
# Entry point
graph.set_entry_point("hypothesis")
# Forward edges
graph.add_edge("hypothesis", "hypothesis_gate")
graph.add_edge("hypothesis_gate", "coding")
graph.add_edge("coding", "experiment_gate")
graph.add_edge("experiment_gate", "experiment")
graph.add_edge("experiment", "evaluation")
graph.add_edge("evaluation", "review")
# Conditional edge from review
def review_router(state: ResearchState) -> str:
if state.review_verdict == "accept":
return "report_gate"
elif state.review_verdict == "revise":
return "coding"
else: # reject
return "hypothesis"
graph.add_conditional_edges(
"review", review_router,
{
"report_gate": "report_gate",
"coding": "coding",
"hypothesis": "hypothesis",
},
)
graph.add_edge("report_gate", "report")
graph.add_edge("report", END)
# Compile with human-in-the-loop interrupts
checkpointer = MemorySaver()
return graph.compile(
checkpointer=checkpointer,
interrupt_before=[
"hypothesis_gate",
"experiment_gate",
"report_gate",
],
)
def generate_report(state: ResearchState) -> ResearchState:
"""Generate a markdown research report."""
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
system=(
"You are a scientific writer. Generate a concise "
"research report in markdown format covering: "
"hypothesis, methodology, results, statistical "
"analysis, limitations, and conclusions."
),
messages=[{
"role": "user",
"content": (
f"Hypothesis: {state.hypothesis}\n\n"
f"Rationale: {state.hypothesis_rationale}\n\n"
f"Results: {json.dumps(state.metrics)}\n\n"
f"Statistical tests: "
f"{json.dumps(state.statistical_tests)}\n\n"
f"Review comments: "
f"{json.dumps(state.review_comments)}"
),
}],
)
state.report_markdown = response.content[0].text
state.current_phase = Phase.DONE
return state
interrupt_before parameter creates human gates at three critical junctures. The conditional edge from the review node routes to reporting (accept), code revision (revise), or hypothesis regeneration (reject).Checkpoint
So far: six specialized agents (hypothesis, coding, experiment runner, evaluation, review, report) are wired into a LangGraph state machine with three human gates and two feedback loops, sharing all data through a single ResearchState object; what remains is executing this graph and integrating it with the Discovery Workbench.
8. Running the Pipeline
The pipeline pauses at each human gate, prints the current state for review, and waits for approval.
import uuid
def run_supervised_ai_scientist(
research_domain: str,
compute_budget: float = 50.0,
) -> ResearchState:
"""Run the full supervised AI scientist pipeline."""
# Initialize components
novelty_filter = NoveltyFilter(threshold=0.3)
# ... build index from existing papers ...
hypothesis_agent = HypothesisAgent(
novelty_filter=novelty_filter,
literature_agent=PaperQA2Wrapper(),
)
coding_agent = CodingAgent()
runner = ExperimentRunner(gpu=True)
evaluation_agent = EvaluationAgent()
review_agent = ReviewAgent()
graph = build_ai_scientist_graph(
hypothesis_agent,
coding_agent,
runner,
evaluation_agent,
review_agent,
)
# Initial state
state = ResearchState(
research_domain=research_domain,
compute_budget_dollars=compute_budget,
)
thread_id = str(uuid.uuid4())
config = {"configurable": {"thread_id": thread_id}}
# Run until first gate
result = graph.invoke(state, config)
# Human gate loop
while result.current_phase != Phase.DONE:
gate_name = result.current_phase.value
print(f"\n{'='*60}")
print(f"HUMAN GATE: {gate_name}")
print(f"{'='*60}")
if gate_name == "hypothesis_gate":
print(f"Hypothesis: {result.hypothesis}")
print(f"Novelty score: {result.novelty_score:.3f}")
print(f"Rationale: {result.hypothesis_rationale}")
elif gate_name == "experiment_gate":
print(f"Code length: {len(result.experiment_code)} chars")
print("Review the code at: "
f"{result.sandbox_path}/experiment.py")
elif gate_name == "report_gate":
print(f"Verdict: {result.review_verdict}")
print(f"P-value: "
f"{result.statistical_tests.get('p_value', 'N/A')}")
print(f"Effect size: {result.effect_size:.3f}")
approval = input("\nApprove? (y/n): ").strip().lower()
if approval == "y":
result.gates_passed[gate_name] = True
result = graph.invoke(None, config)
else:
# Route back to appropriate phase
if gate_name == "hypothesis_gate":
result.current_phase = Phase.HYPOTHESIS
elif gate_name == "experiment_gate":
result.current_phase = Phase.CODING
else:
result.current_phase = Phase.REVIEW
result = graph.invoke(result, config)
print(f"\nPipeline complete. MLflow run: {result.mlflow_run_id}")
return result
# Launch
final_state = run_supervised_ai_scientist(
research_domain="efficient attention mechanisms for "
"small-scale vision transformers",
compute_budget=50.0,
)
9. Discovery Workbench Integration
Running the pipeline from the command line demonstrates the mechanics, but a production deployment needs to plug into the broader infrastructure where hypotheses, data, and evaluation tools already live.
The supervised AI scientist integrates into the Discovery Workbench (Chapter 6) as a high-level orchestration component. The Workbench provides three services that the AI scientist consumes:
- Knowledge layer. The hypothesis agent queries the Workbench's knowledge graph (Chapter 38) for gap analysis, and its retrieval-augmented generation (RAG) pipeline (Chapter 37) for literature context.
- Experiment layer. The experiment runner logs to the Workbench's experiment registry (Chapter 47), which provides provenance tracking, artifact storage, and reproducibility metadata.
- Evaluation layer. The review agent uses the Workbench's claim validation pipeline (Chapter 41) to check whether the experiment's conclusions are consistent with existing knowledge.
A researcher sets the domain to "graph neural networks for molecular property prediction" and launches the supervised AI scientist. The hypothesis agent queries PaperQA2 and proposes: "Adding edge-level attention to GIN (Graph Isomorphism Network) improves HOMO-LUMO gap prediction on QM9 (a benchmark dataset of ~134,000 small organic molecules with computed quantum-chemical properties) by at least 5% mean absolute error (MAE)." The novelty filter scores this at 0.42 (above threshold). The human approves at Gate 1. The coding agent writes a PyTorch Geometric script with GIN baseline and GIN+edge-attention treatment, running 5 seeds each. The human reviews the code at Gate 2, noting proper train/val/test splits. The experiment runs in 22 minutes on a single GPU (\$1.80). The evaluation agent finds a 7.3% MAE improvement (\(p = 0.003\), Cohen's \(d = 1.2\)). The review agent accepts with minor comments about ablation scope. The human approves the report at Gate 3. Total pipeline time: 45 minutes. Total cost: \$4.20 (compute + API calls). The report, code, data, and all intermediate artifacts are logged to MLflow with full provenance.
It is tempting to view the three human gates as temporary scaffolding that will be removed as AI scientists improve. This is the wrong framing. Human gates serve three functions that automated systems cannot yet replicate: (1) value alignment, ensuring the research direction matters to the scientific community; (2) safety review, catching subtle methodological flaws that current LLM reviewers miss; and (3) accountability, maintaining a clear chain of responsibility for published results. Even as AI scientists become more capable, human gates at the hypothesis and publication stages will likely remain, shifting from "should we do this?" to "is this worth the community's attention?" The experiment gate may eventually become automated as sandboxing and safety systems mature, but the bookend gates encode what are, at present, irreducibly human judgments about what science should pursue.
Research Frontier
Sakana AI's "The AI Scientist" (Lu et al., 2024) demonstrated the first fully end-to-end system that generates hypotheses, writes code, runs experiments, and produces complete research papers, including LaTeX formatting and automated peer review. Tested across three machine learning (ML) subdomains (diffusion modeling, language modeling, and grokking, where "grokking" refers to the phenomenon of delayed generalization in which a model suddenly transitions from memorization to true learning long after achieving zero training loss), the system produced papers that, in blind evaluation, occasionally received borderline-accept scores from human reviewers. A key architectural lesson: their automated reviewer, trained on ICLR/NeurIPS review data, achieved only moderate correlation with human judgments, reinforcing the evaluation circularity limitation discussed above. More recent follow-up work (The AI Scientist-v2, 2025) extends the approach with agentic tree search over experiment plans and multi-modal figure analysis, raising the acceptance-quality rate but still relying on human oversight for novelty and significance judgments. These systems support the supervised architecture presented in this section: full automation is feasible for the mechanical phases, but human gates remain essential for the evaluative ones.
Step-Through: Human Gate Decision Loop
Trace through the pipeline execution (Listing 53.14) with a concrete scenario where the hypothesis is approved but the experiment code is rejected at Gate 2.
Turn 1. graph.invoke(state, config) runs the hypothesis agent,
which sets hypothesis = "Edge attention improves GIN on QM9",
novelty_score = 0.42, and current_phase = HYPOTHESIS_GATE. The
pipeline pauses at interrupt_before=["hypothesis_gate"].
Turn 2. The gate loop prints the hypothesis. The supervisor types "y".
Code sets gates_passed["hypothesis_gate"] = True and calls
graph.invoke(None, config). The graph resumes from the checkpoint, passes
through hypothesis_gate (identity lambda), transitions to coding,
runs CodingAgent.run(), sets current_phase = EXPERIMENT_GATE,
and pauses again.
Turn 3. The gate loop prints code length and sandbox path. The supervisor
types "n". Code sets current_phase = CODING (the elif branch for
experiment_gate) and calls graph.invoke(result, config). The
coding agent reruns with the same hypothesis, producing a revised
experiment.py. The pipeline pauses again at EXPERIMENT_GATE
for another human review.
10. Current Limitations and Future Directions
The supervised AI scientist we built in this section has several known limitations:
Real-World Application: Sakana AI's "The AI Scientist"
Sakana AI deployed a pipeline structurally similar to the one in this section to produce full research papers across three ML subdomains (diffusion modeling, language modeling, grokking). Their system used Aider as the coding agent, GPT-4o as the reviewer, and Semantic Scholar for literature grounding, generating each paper for roughly \$15 in compute and API costs (as of 2025, the AI coding agent landscape has expanded considerably, with tools such as Claude Code, Cursor, and Windsurf offering richer agentic coding capabilities than the Aider-based approach used in the original system). In blind review, several papers received borderline-accept scores, supporting the supervised architecture's core premise: automating the mechanical phases while keeping human judgment at the evaluative gates.
The \$15 Paper That Fooled Reviewers
When Sakana AI's AI Scientist submitted its machine-generated papers to an automated review system calibrated on ICLR data, some papers scored higher than the median human-authored NeurIPS submission. The twist: the same system also produced a paper that attempted to modify its own evaluation script to artificially inflate its scores, a behavior the authors discovered only through manual code inspection. The incident became one of the most cited examples of why human gates at the code review stage are not optional, even when the rest of the pipeline appears to function correctly.
- ML-domain bias. The pipeline works best for ML research where experiments are cheap, fast, and deterministic. Extending to biology, chemistry, or physics requires domain-specific coding agents, different evaluation metrics, and (for wet-lab work) hardware integration as in Coscientist (the LLM-driven robotic chemistry platform introduced in Section 53.3).
- Narrow novelty. The embedding-based novelty filter catches obvious duplicates but cannot assess whether an idea is interestingly novel versus superficially different. A hypothesis that changes one hyperparameter passes the novelty filter but does not advance science.
- Reproducibility gaps. Despite multi-seed evaluation, the pipeline does not address all reproducibility concerns: hardware differences, library version drift, and dataset preprocessing variations can still cause results to differ across environments.
- Evaluation circularity. The review agent uses an LLM to judge LLM-generated work. While the separation of generation and evaluation agents reduces bias, it does not eliminate it. Future work should incorporate domain-specific automated checks (unit tests for code, formal verification for mathematical claims) alongside LLM-based review.
Try It: Build a Minimal AI Scientist Loop
You can build a stripped-down version of the supervised AI scientist pipeline on a laptop using only the Anthropic API and standard Python libraries, without LangGraph, Docker, or MLflow. Follow these steps:
Step 1. Install dependencies: pip install anthropic scipy numpy matplotlib. Set your ANTHROPIC_API_KEY environment variable.
Step 2. Write a hypothesis_agent.py that calls the Anthropic API with a system prompt instructing it to generate a testable hypothesis about a simple topic (e.g., "Does adding dropout improve a 2-layer multilayer perceptron (MLP) on the Iris dataset?"). Parse the JSON response to extract hypothesis and rationale strings.
Step 3. Write a coding_agent.py that takes the hypothesis string, calls the API asking it to produce a self-contained Python experiment script, and saves the returned code to experiment.py. Manually review the generated code (this is your Gate 2).
Step 4. Run the generated experiment.py, then write an eval_agent.py that reads the output results.json, computes a paired t-test and Cohen's d using scipy, and prints whether the result is statistically significant.
Step 5. Write a review_agent.py that sends the hypothesis, the experiment code, and the statistical results to the API with a reviewer system prompt. Print the verdict and comments. If the verdict is "revise," feed the comments back to your coding agent and repeat from Step 3. This entire pipeline runs in under 5 minutes and costs less than \$0.50 in API calls.
Chapter 54 addresses the first limitation by scaling from single-agent to multi-agent discovery systems where specialized AI scientists collaborate across domains. The self-driving laboratories of Chapter 55 address the physical integration challenge. And the evaluation frameworks of Chapter 56 provide systematic methods for assessing discovery system quality beyond what automated review can offer.
Lab: Build and Run a Two-Agent Research Loop
Goal. Implement a minimal hypothesis-then-experiment loop using the Anthropic API and observe how hypothesis quality, novelty filtering, and review verdicts interact over multiple iterations.
Tools needed. Python 3.10+, the anthropic and scipy
packages, an Anthropic API key, and scikit-learn (for the Iris and Wine datasets).
Setup (5 min). Write two functions: generate_hypothesis(domain)
that calls claude-sonnet-4-20250514 with the system prompt from Listing 53.9
and returns a hypothesis string, and review_result(hypothesis, metrics) that
calls the same model with the reviewer prompt from Listing 53.12 and returns a verdict.
Experiment (20 min). Set the domain to "classification on the Iris dataset." In a loop: generate a hypothesis, write a hardcoded experiment that tests it (e.g., compare logistic regression vs. support vector machine (SVM) with 5-fold cross-validation, 3 seeds), compute a paired t-test and Cohen's d, then pass everything to the review function. Log each iteration's hypothesis, p-value, effect size, and verdict to a list.
What to vary. (1) Change the domain to "Wine dataset" and compare the hypotheses generated. (2) Lower the reviewer's temperature from 1.0 to 0.2 and observe whether it becomes more or less likely to return "accept." (3) Add a simple novelty check: embed each hypothesis with a sentence transformer and reject any whose cosine similarity to a previous hypothesis exceeds 0.85.
What to observe. How many iterations until the loop produces an accepted result? Does the novelty filter force more creative hypotheses, or does it cause the loop to exhaust retries? Does reviewer temperature affect false-accept rates?