use clap::Parser;
use ripmap::training::reasoning::Agent;
use ripmap::training_outer::{
OuterConfig, OuterLoop, OuterScratchpad, Promptgram, RunConfig, RunContext, baseline_promptgram,
};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "ripmap-train-outer")]
#[command(about = "L2 Promptgram Optimizer")]
#[command(help_template = "\
{name} - {about}
{usage-heading} {usage}
{all-args}
")]
struct Args {
run_name: String,
#[arg(long, default_value = "10", value_name = "N")]
steps_outer: usize,
#[arg(long, default_value = "20", value_name = "N")]
episodes_inner: usize,
#[arg(long, default_value = "claude", value_name = "AGENT")]
agent_inner: String,
#[arg(long, default_value = "codex", value_name = "AGENT")]
agent_outer: String,
#[arg(long, value_name = "PATH")]
promptgram: Option<String>,
#[arg(long)]
dry: bool,
#[arg(long)]
resume: bool,
#[arg(long, default_value = "curated", value_name = "NAME")]
corpus: String,
#[arg(long, default_value = "1", value_name = "N")]
save_interval: usize,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
use owo_colors::OwoColorize;
let args = Args::parse();
println!();
println!("{}", " RIPMAP L2 OUTER LOOP OPTIMIZER ".bold().on_magenta());
println!();
let inner_agent: Agent = args.agent_inner.parse().map_err(|e: String| e)?;
let _outer_agent: Agent = args.agent_outer.parse().map_err(|e: String| e)?;
let run_config = RunConfig {
run_name: args.run_name.clone(),
base_dir: PathBuf::from("training-outer/runs"),
episodes: args.episodes_inner,
agent: inner_agent,
model: None,
save_interval: args.save_interval,
};
let ctx = RunContext {
config: run_config.clone(),
episode: 0,
is_outer: true,
parent_step: None,
};
ctx.setup_dirs()?;
let scratchpad_path = ctx.config.output_dir().join("outer_scratchpad.json");
let mut outer_scratchpad = if args.resume && scratchpad_path.exists() {
println!("📂 Resuming from existing scratchpad...");
let content = std::fs::read_to_string(&scratchpad_path)?;
serde_json::from_str(&content)?
} else {
OuterScratchpad::default()
};
let inner_promptgram = if let Some(ref source) = args.promptgram {
let path = if source.contains('/') || source.ends_with(".toml") {
PathBuf::from(source)
} else {
PathBuf::from(format!("training-outer/runs/{}/promptgram.toml", source))
};
println!("📄 Loading promptgram from {:?}", path);
Promptgram::load(&path)?
} else {
println!("📄 Using default inner promptgram");
baseline_promptgram()
};
let edit_prompts = !args.dry;
let mut outer_loop = OuterLoop::new();
outer_loop.population = vec![inner_promptgram];
outer_loop.inner_config = OuterConfig {
inner_episodes: args.episodes_inner,
inner_agent: args.agent_inner.clone(),
outer_agent: args.agent_outer.clone(),
max_outer_steps: args.steps_outer,
exploration_quota: 0.2,
corpus: args.corpus.clone(),
edit_prompts,
..Default::default()
};
println!("Configuration:");
println!(" Run name: {}", args.run_name);
println!(" Outer steps: {}", args.steps_outer);
println!(" Inner episodes: {}", args.episodes_inner);
println!(" Inner agent: {}", args.agent_inner);
println!(" Outer agent: {}", args.agent_outer);
println!(" Corpus: {}", args.corpus);
println!(
" L2 reasoning: {}",
if edit_prompts { "enabled" } else { "dry run" }
);
println!(" Output dir: {:?}", ctx.config.output_dir());
println!();
let start_step = outer_scratchpad.episodes.len();
if start_step > 0 {
println!(
"📊 Resuming from step {} (best NDCG: {:.4})",
start_step, outer_scratchpad.best_ndcg
);
}
let config_path = ctx.config.output_dir().join("config.toml");
let config_content = format!(
r#"# L2 Outer Loop Configuration
[run]
name = "{}"
steps_outer = {}
episodes_inner = {}
agent_inner = "{}"
agent_outer = "{}"
corpus = "{}"
dry = {}
"#,
args.run_name,
args.steps_outer,
args.episodes_inner,
args.agent_inner,
args.agent_outer,
args.corpus,
args.dry,
);
std::fs::write(&config_path, config_content)?;
println!("{}", "─".repeat(65));
println!(
"Starting outer loop ({} steps)...\n",
args.steps_outer - start_step
);
for step in start_step..args.steps_outer {
println!(
"{}",
format!(" OUTER STEP {}/{} ", step + 1, args.steps_outer)
.bold()
.on_cyan()
);
match outer_loop.run_outer_episode(step + 1, &ctx, &mut outer_scratchpad) {
Ok(summary) => {
println!("\n 📈 Results:");
println!(
" NDCG: {:.4} → {:.4} (Δ{:+.4})",
summary.baseline_metrics.ndcg, summary.final_metrics.ndcg, summary.delta.ndcg
);
println!(
" Failures: {} → {}",
summary.baseline_metrics.failures, summary.final_metrics.failures
);
println!(" Duration: {:.1}s", summary.duration_secs);
println!(
" Stability: {} collapses, {:.4} variance",
summary.stability.collapse_events, summary.stability.ndcg_variance
);
println!(
" Meta-levers: {}",
summary.meta_levers_estimate.summary()
);
if !summary.strategy_capsules.is_empty() {
println!(" Strategy capsules:");
for capsule in summary.strategy_capsules.iter().take(3) {
println!(" • {}", capsule.chars().take(60).collect::<String>());
}
}
if summary.final_metrics.ndcg > outer_scratchpad.best_ndcg {
println!(
"\n 🏆 NEW BEST! {:.4} → {:.4}",
outer_scratchpad.best_ndcg, summary.final_metrics.ndcg
);
}
}
Err(e) => {
eprintln!("\n ❌ Error in outer step: {}", e);
}
}
let scratchpad_json = serde_json::to_string_pretty(&outer_scratchpad)?;
std::fs::write(&scratchpad_path, &scratchpad_json)?;
println!("\n 💾 Checkpoint saved\n");
}
println!();
println!("{}", "─".repeat(65));
println!("{}", " OUTER LOOP COMPLETE ".bold().on_green());
println!("{}", "─".repeat(65));
print!("{}", outer_scratchpad.statistical_summary());
if !outer_scratchpad.episodes.is_empty() {
let all_insights: std::collections::HashSet<_> = outer_scratchpad
.episodes
.iter()
.flat_map(|e| e.structural_insights.iter())
.collect();
if !all_insights.is_empty() {
println!("═══ STRUCTURAL INSIGHTS ═══");
for insight in all_insights.iter().take(5) {
println!(" • {}", insight);
}
println!();
}
let all_capsules: Vec<_> = outer_scratchpad
.episodes
.iter()
.flat_map(|e| e.strategy_capsules.iter())
.collect();
println!("Total strategy capsules recorded: {}", all_capsules.len());
}
println!("\nOutputs saved to: {:?}", ctx.config.output_dir());
Ok(())
}