use anyhow::Result;
use std::io::IsTerminal;
use std::path::PathBuf;
#[derive(Debug, Clone, Copy)]
pub enum ExecutionMode {
Cautious,
Balanced,
Yolo,
}
impl ExecutionMode {
fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"cautious" => ExecutionMode::Cautious,
"yolo" => ExecutionMode::Yolo,
_ => ExecutionMode::Balanced,
}
}
}
#[allow(clippy::too_many_arguments)]
pub async fn run(
task: String,
workdir: Option<PathBuf>,
auto_approve: bool,
complexity_k: usize,
mode: String,
model: Option<String>,
architect_model: Option<String>,
actuator_model: Option<String>,
verifier_model: Option<String>,
speculator_model: Option<String>,
defer_tests: bool,
log_llm: bool,
single_file: bool,
verifier_strictness: String,
architect_fallback_model: Option<String>,
actuator_fallback_model: Option<String>,
verifier_fallback_model: Option<String>,
speculator_fallback_model: Option<String>,
output_plan: Option<PathBuf>,
) -> Result<()> {
let working_dir = workdir.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
let exec_mode = ExecutionMode::from_str(&mode);
let architect = model.clone().or(architect_model);
let actuator = model.clone().or(actuator_model);
let verifier = model.clone().or(verifier_model);
let speculator = model.or(speculator_model);
let default_model = perspt_agent::ModelTier::default_model_name();
log::info!("Starting SRBN agent");
log::info!(" Task: {}", task);
log::info!(" Working directory: {:?}", working_dir);
log::info!(" Auto-approve: {}", auto_approve);
log::info!(" Complexity K: {}", complexity_k);
log::info!(" Defer tests: {}", defer_tests);
log::info!(" Log LLM: {}", log_llm);
log::info!(" Mode: {:?}", exec_mode);
log::info!(
" Architect model: {}",
architect.as_deref().unwrap_or_else(|| {
log::debug!("Using default");
default_model
})
);
log::info!(
" Actuator model: {}",
actuator.as_deref().unwrap_or(default_model)
);
log::info!(
" Verifier model: {}",
verifier.as_deref().unwrap_or(default_model)
);
log::info!(
" Speculator model: {}",
speculator.as_deref().unwrap_or(default_model)
);
let mut orchestrator = perspt_agent::SRBNOrchestrator::new_with_models(
working_dir.clone(),
auto_approve,
architect,
actuator,
verifier,
speculator,
architect_fallback_model,
actuator_fallback_model,
verifier_fallback_model,
speculator_fallback_model,
);
orchestrator.context.complexity_k = complexity_k;
orchestrator.context.defer_tests = defer_tests;
orchestrator.context.log_llm = log_llm;
if single_file {
orchestrator.context.execution_mode = perspt_core::types::ExecutionMode::Solo;
}
orchestrator.context.verifier_strictness = match verifier_strictness.to_lowercase().as_str() {
"strict" => perspt_core::types::VerifierStrictness::Strict,
"minimal" => perspt_core::types::VerifierStrictness::Minimal,
_ => perspt_core::types::VerifierStrictness::Default,
};
println!("🚀 SRBN Agent starting...");
println!(" Session: {}", orchestrator.session_id());
println!(" Task: {}", task);
{
let registry = perspt_core::plugin::PluginRegistry::new();
let detected = registry.detect_all(&working_dir);
if detected.is_empty() {
println!(" Plugins: none detected yet (will re-detect after init)");
} else {
let names: Vec<&str> = detected.iter().map(|p| p.name()).collect();
println!(" Plugins (provisional): {}", names.join(", "));
}
}
println!();
let is_tty = std::io::stdout().is_terminal();
if is_tty && !auto_approve {
println!("Running in interactive TUI mode...");
println!("(Use --yes flag to run headlessly)");
println!();
perspt_tui::run_agent_tui_with_orchestrator(orchestrator, task).await?;
} else {
println!(
"Running in headless mode (auto-approve={})...",
auto_approve
);
println!();
match orchestrator.run(task.clone()).await {
Ok(()) => {
println!();
println!("✅ Task completed successfully!");
println!(" Nodes processed: {}", orchestrator.node_count());
if let Some(ref plan_path) = output_plan {
let nodes: Vec<_> = orchestrator
.graph
.node_indices()
.map(|idx| &orchestrator.graph[idx])
.collect();
match serde_json::to_string_pretty(&nodes) {
Ok(json) => {
if let Err(e) = std::fs::write(plan_path, &json) {
eprintln!(
"⚠️ Failed to write plan to {}: {}",
plan_path.display(),
e
);
} else {
println!(" Plan exported to {}", plan_path.display());
}
}
Err(e) => {
eprintln!("⚠️ Failed to serialize plan: {}", e);
}
}
}
let sid = orchestrator.session_id().to_string();
if let Ok(store) = perspt_store::SessionStore::new() {
if let Ok(nodes) = store.get_node_states(&sid) {
let completed = nodes
.iter()
.filter(|n| n.state == "COMPLETED" || n.state == "STABLE")
.count();
let failed = nodes.iter().filter(|n| n.state == "FAILED").count();
let retries: i32 = nodes.iter().map(|n| n.attempt_count.max(0)).sum();
println!();
println!(
"[VERIFY] {}/{} nodes completed, {} failed, {} retries",
completed,
nodes.len(),
failed,
retries
);
if let Some(latest) = nodes.last() {
if let Ok(history) = store.get_energy_history(&sid, &latest.node_id) {
if let Some(e) = history.last() {
println!("[ENERGY] V(x)={:.3} syn={:.2} str={:.2} log={:.2} boot={:.2} sheaf={:.2}",
e.v_total, e.v_syn, e.v_str, e.v_log, e.v_boot, e.v_sheaf);
}
}
}
}
if let Ok(escalations) = store.get_escalation_reports(&sid) {
if !escalations.is_empty() {
println!("[ESCALATE] {} escalation(s) recorded", escalations.len());
}
}
if let Ok(branches) = store.get_provisional_branches(&sid) {
if !branches.is_empty() {
let merged = branches.iter().filter(|b| b.state == "merged").count();
let flushed = branches.iter().filter(|b| b.state == "flushed").count();
println!(
"[BRANCH] {} total, {} merged, {} flushed",
branches.len(),
merged,
flushed
);
}
}
println!("[COMMIT] Session {} complete", &sid[..sid.len().min(16)]);
}
}
Err(e) => {
println!();
println!("❌ Task failed: {}", e);
return Err(e);
}
}
}
println!();
println!("✓ Agent session completed");
Ok(())
}