use super::*;
impl SRBNOrchestrator {
pub(super) async fn run_solo_mode(&mut self, task: String) -> Result<()> {
const MAX_ATTEMPTS: usize = 3;
const EPSILON: f32 = 0.1;
let mut current_prompt = self.build_solo_prompt(&task);
let mut attempt = 0;
let mut last_filename: String;
let mut last_code: String;
loop {
attempt += 1;
if self.is_abort_requested() {
self.emit_log("⚠️ Session aborted — stopping solo execution".to_string());
anyhow::bail!("Solo Mode aborted by user");
}
if attempt > MAX_ATTEMPTS {
self.emit_log(format!(
"Solo Mode failed after {} attempts, consider Team Mode",
MAX_ATTEMPTS
));
self.emit_event(perspt_core::AgentEvent::Complete {
success: false,
message: "Solo Mode exhausted retries".to_string(),
});
anyhow::bail!("Solo Mode failed after {} attempts", MAX_ATTEMPTS);
}
self.emit_log(format!("Solo Mode attempt {}/{}", attempt, MAX_ATTEMPTS));
let response = self
.call_llm_with_logging(&self.actuator_model.clone(), ¤t_prompt, Some("solo"))
.await?;
let (bundle_opt, parse_state, _) =
self.parse_artifact_bundle_typed(&response, "solo", attempt as u32);
let (filename, code) = match bundle_opt {
Some(ref bundle) if !bundle.artifacts.is_empty() => {
let first = &bundle.artifacts[0];
let content = match first {
perspt_core::types::ArtifactOperation::Write { content, .. } => {
content.clone()
}
perspt_core::types::ArtifactOperation::Diff { patch, .. } => patch.clone(),
_ => String::new(),
};
(first.path().to_string(), content)
}
_ => {
self.emit_log(format!(
"No code block found in LLM response ({})",
parse_state
));
continue;
}
};
last_filename = filename.clone();
last_code = code.clone();
let full_path = self.context.working_dir.join(&filename);
let mut args = HashMap::new();
args.insert("path".to_string(), filename.clone());
args.insert("content".to_string(), code.clone());
let call = ToolCall {
name: "write_file".to_string(),
arguments: args,
};
let result = self.tools.execute(&call).await;
if !result.success {
self.emit_log(format!("Failed to write {}: {:?}", filename, result.error));
continue;
}
self.emit_log(format!("Created: {}", filename));
self.last_written_file = Some(full_path.clone());
let energy = self.solo_verify(&full_path).await;
let v_total = energy.total_simple();
self.emit_log(format!(
"V(x) = {:.2} (V_syn={:.2}, V_log={:.2}, V_boot={:.2})",
v_total, energy.v_syn, energy.v_log, energy.v_boot
));
if v_total < EPSILON {
self.emit_log(format!(
"Solo Mode complete! V(x)={:.2} < epsilon={:.2}",
v_total, EPSILON
));
self.emit_event(perspt_core::AgentEvent::Complete {
success: true,
message: format!("Created {}", filename),
});
return Ok(());
}
self.emit_log(format!(
"Unstable (V={:.2} > epsilon={:.2}), building correction prompt...",
v_total, EPSILON
));
current_prompt =
self.build_solo_correction_prompt(&task, &last_filename, &last_code, &energy);
}
}
async fn solo_verify(&mut self, path: &std::path::Path) -> EnergyComponents {
let mut energy = EnergyComponents::default();
let lsp_key = self.lsp_key_for_file(&path.to_string_lossy());
if let Some(client) = lsp_key.as_deref().and_then(|k| self.lsp_clients.get(k)) {
tokio::time::sleep(tokio::time::Duration::from_millis(300)).await;
let path_str = path.to_string_lossy().to_string();
let diagnostics = client.get_diagnostics(&path_str).await;
energy.v_syn = LspClient::calculate_syntactic_energy(&diagnostics);
if !diagnostics.is_empty() {
self.emit_log(format!(
"LSP: {} diagnostics (V_syn={:.2})",
diagnostics.len(),
energy.v_syn
));
self.context.last_diagnostics = diagnostics;
}
}
energy.v_log = self.run_doctest(path).await;
energy.v_boot = self.run_script_check(path).await;
energy
}
async fn run_script_check(&mut self, path: &std::path::Path) -> f32 {
let output = tokio::process::Command::new("python")
.arg(path)
.current_dir(&self.context.working_dir)
.output()
.await;
match output {
Ok(out) if out.status.success() => {
self.emit_log("Script execution: OK".to_string());
0.0
}
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
let stdout = String::from_utf8_lossy(&out.stdout);
let error_output = if !stderr.is_empty() {
stderr.to_string()
} else {
stdout.to_string()
};
let truncated = if error_output.len() > 500 {
format!("{}...(truncated)", &error_output[..500])
} else {
error_output.clone()
};
self.emit_log(format!("Script execution: FAILED\n{}", truncated));
self.context.last_test_output = Some(error_output);
5.0 }
Err(e) => {
self.emit_log(format!("Script execution: ERROR ({})", e));
5.0
}
}
}
fn build_solo_prompt(&self, task: &str) -> String {
let ev = perspt_core::types::PromptEvidence {
user_goal: Some(task.to_string()),
..Default::default()
};
crate::prompt_compiler::compile(perspt_core::types::PromptIntent::SoloGenerate, &ev).text
}
fn build_solo_correction_prompt(
&self,
task: &str,
filename: &str,
current_code: &str,
energy: &EnergyComponents,
) -> String {
let mut errors = Vec::new();
for diag in &self.context.last_diagnostics {
let severity = match diag.severity {
Some(lsp_types::DiagnosticSeverity::ERROR) => "ERROR",
Some(lsp_types::DiagnosticSeverity::WARNING) => "WARNING",
Some(lsp_types::DiagnosticSeverity::INFORMATION) => "INFO",
Some(lsp_types::DiagnosticSeverity::HINT) => "HINT",
_ => "DIAGNOSTIC",
};
errors.push(format!(
"- Line {}: {} [{}]",
diag.range.start.line + 1,
diag.message,
severity
));
}
if let Some(ref output) = self.context.last_test_output {
if !output.is_empty() {
let truncated = if output.len() > 1000 {
format!("{}...(truncated)", &output[..1000])
} else {
output.clone()
};
errors.push(format!("- Runtime/Test Error:\n{}", truncated));
}
}
let error_list = if errors.is_empty() {
"No specific errors captured, but energy is still too high.".to_string()
} else {
errors.join("\n")
};
let ev = perspt_core::types::PromptEvidence {
user_goal: Some(task.to_string()),
solo_file_path: Some(filename.to_string()),
existing_file_contents: vec![(filename.to_string(), current_code.to_string())],
verifier_diagnostics: Some(format!(
"Energy: V_syn={:.2}, V_log={:.2}, V_boot={:.2}\n\n{}",
energy.v_syn, energy.v_log, energy.v_boot, error_list
)),
..Default::default()
};
crate::prompt_compiler::compile(perspt_core::types::PromptIntent::SoloCorrect, &ev).text
}
async fn run_doctest(&mut self, file_path: &std::path::Path) -> f32 {
let output = tokio::process::Command::new("python")
.args(["-m", "doctest", "-v"])
.arg(file_path)
.current_dir(&self.context.working_dir)
.output()
.await;
match output {
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
let failed = stderr.matches("FAILED").count() + stdout.matches("FAILED").count();
let passed = stdout.matches("ok").count();
if failed > 0 {
self.emit_log(format!("Doctest: {} passed, {} failed", passed, failed));
let doctest_output = format!("{}\n{}", stdout, stderr);
self.context.last_test_output = Some(doctest_output);
2.0 * (failed as f32)
} else if passed > 0 {
self.emit_log(format!("Doctest: {} passed", passed));
0.0
} else {
log::debug!("No doctests found in file");
0.0
}
}
Err(e) => {
log::warn!("Failed to run doctest: {}", e);
0.0 }
}
}
}