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>,
energy_weights: String,
stability_threshold: f32,
max_cost: f32,
max_steps: usize,
) -> 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,
};
{
let max_s = if max_steps > 0 {
Some(max_steps as u32)
} else {
None
};
let max_c = if max_cost > 0.0 {
Some(max_cost as f64)
} else {
None
};
orchestrator.set_budget(max_s, None, max_c);
}
orchestrator.stability_epsilon = stability_threshold;
{
let parts: Vec<f32> = energy_weights
.split(',')
.filter_map(|s| s.trim().parse().ok())
.collect();
if parts.len() == 3 {
orchestrator.energy_alpha = parts[0];
orchestrator.energy_beta = parts[1];
orchestrator.energy_gamma = parts[2];
}
}
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!();
let abort_flag = orchestrator.abort_flag();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.ok();
eprintln!(
"\n⚠️ Interrupt received. Aborting gracefully... (Ctrl+C again to force quit)"
);
abort_flag.store(true, std::sync::atomic::Ordering::Relaxed);
tokio::signal::ctrl_c().await.ok();
eprintln!("\nForce quitting...");
std::process::exit(130);
});
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() {
use perspt_core::types::NodeState;
if let Ok(nodes) = store.get_node_states(&sid) {
let completed = nodes
.iter()
.filter(|n| NodeState::from_display_str(&n.state).is_success())
.count();
let failed = nodes
.iter()
.filter(|n| {
let s = NodeState::from_display_str(&n.state);
s == NodeState::Failed || s == NodeState::Escalated
})
.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
);
}
}
if let Ok(Some(budget)) = store.get_budget_envelope(&sid) {
let steps_str = budget
.max_steps
.map(|m| format!("{}/{}", budget.steps_used, m))
.unwrap_or_else(|| format!("{}", budget.steps_used));
let cost_str = budget
.max_cost_usd
.map(|m| format!("${:.2}/${:.2}", budget.cost_used_usd, m))
.unwrap_or_else(|| format!("${:.2}", budget.cost_used_usd));
println!(
"[BUDGET] steps={} cost={} revisions={}",
steps_str, cost_str, budget.revisions_used
);
}
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(())
}