use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
use anyhow::{anyhow, bail, Context, Result};
use super::catalog::QuantSpec;
use crate::bench_harness::subprocess::{apply_env_allowlist, run_with_group_timeout};
#[derive(Clone, Debug)]
pub struct LlamaServerOpts {
pub port: u16,
pub ctx: u32,
pub parallel: u32,
pub binary: PathBuf,
pub models_dir: PathBuf,
pub tensor_split: Option<String>,
pub boot_timeout: Duration,
pub host: String,
}
impl Default for LlamaServerOpts {
fn default() -> Self {
Self {
port: 8000,
ctx: 262_144,
parallel: 2,
binary: resolve_llama_binary(),
models_dir: resolve_models_dir(),
tensor_split: Some("24,24".into()),
boot_timeout: Duration::from_secs(180),
host: "127.0.0.1".into(),
}
}
}
pub fn resolve_llama_binary() -> PathBuf {
if let Ok(p) = std::env::var("LLAMA_SERVER_BIN") {
if !p.is_empty() {
return PathBuf::from(p);
}
}
if let Ok(out) = Command::new("which").arg("llama-server").output() {
if out.status.success() {
let path = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !path.is_empty() {
return PathBuf::from(path);
}
}
}
PathBuf::from("/home/ivo/llama.cpp/build/bin/llama-server")
}
pub fn resolve_models_dir() -> PathBuf {
if let Ok(p) = std::env::var("SWEBENCH_MODELS_DIR") {
if !p.is_empty() {
return PathBuf::from(p);
}
}
let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
home.join("models").join("qwen36-quants")
}
pub fn build_llama_server_args(
spec: &QuantSpec,
opts: &LlamaServerOpts,
gguf: &Path,
mmproj: Option<&Path>,
) -> Vec<String> {
let mut args: Vec<String> = vec![
"-m".into(),
gguf.to_string_lossy().into_owned(),
"--jinja".into(),
"-c".into(),
opts.ctx.to_string(),
"-ngl".into(),
"99".into(),
];
if let Some(ts) = opts.tensor_split.as_ref() {
if !ts.trim().is_empty() {
args.push("--tensor-split".into());
args.push(ts.clone());
}
}
args.extend([
"-ctk".into(),
"q8_0".into(),
"-ctv".into(),
"q8_0".into(),
"--parallel".into(),
opts.parallel.to_string(),
"--cont-batching".into(),
"--host".into(),
opts.host.clone(),
"--port".into(),
opts.port.to_string(),
"--alias".into(),
spec.alias.clone(),
]);
if let Some(p) = mmproj {
args.push("--mmproj".into());
args.push(p.to_string_lossy().into_owned());
}
args
}
pub struct LlamaServer {
pub child: Option<Child>,
pub alias: String,
pub port: u16,
}
impl LlamaServer {
pub fn stop_existing(port: u16) {
use sysinfo::{ProcessesToUpdate, System};
let mut sys = System::new();
sys.refresh_processes(ProcessesToUpdate::All, true);
for proc_ in sys.processes().values() {
if !proc_.name().to_string_lossy().contains("llama-server") {
continue;
}
let cmd: Vec<String> = proc_
.cmd()
.iter()
.map(|s| s.to_string_lossy().into_owned())
.collect();
if cmd_targets_port(&cmd, port) {
proc_.kill();
}
}
std::thread::sleep(Duration::from_secs(2));
}
pub fn boot(spec: &QuantSpec, opts: &LlamaServerOpts) -> Result<Self> {
let gguf = opts.models_dir.join(&spec.gguf);
let mmproj = opts.models_dir.join(&spec.mmproj);
let gguf = gguf
.canonicalize()
.with_context(|| format!("missing GGUF: {}", gguf.display()))?;
let canon_mmproj = mmproj.canonicalize().ok();
let argv = build_llama_server_args(spec, opts, &gguf, canon_mmproj.as_deref());
let mut cmd = Command::new(&opts.binary);
cmd.args(&argv);
let log_path = format!("/tmp/llama-{}.log", spec.alias);
let log = std::fs::File::create(&log_path)
.with_context(|| format!("opening log file {}", log_path))?;
let log_err = log.try_clone()?;
cmd.stdout(Stdio::from(log)).stderr(Stdio::from(log_err));
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
cmd.process_group(0);
}
let child = cmd
.spawn()
.with_context(|| format!("spawning {}", opts.binary.display()))?;
let pid = child.id();
let port = opts.port;
let alias = spec.alias.clone();
let server = LlamaServer {
child: Some(child),
alias: alias.clone(),
port,
};
let deadline = Instant::now() + opts.boot_timeout;
let mut probe_delay = Duration::from_millis(100);
while Instant::now() < deadline {
if probe_endpoint(port) {
return Ok(server);
}
std::thread::sleep(probe_delay);
probe_delay = (probe_delay * 2).min(Duration::from_secs(2));
}
drop(server);
bail!("llama-server boot timed out (alias={}, pid={})", alias, pid);
}
}
impl Drop for LlamaServer {
fn drop(&mut self) {
if let Some(mut child) = self.child.take() {
let _ = child.kill();
let _ = child.wait();
}
}
}
fn cmd_targets_port(cmd: &[String], port: u16) -> bool {
let p = port.to_string();
cmd.windows(2).any(|w| w[0] == "--port" && w[1] == p)
}
fn probe_endpoint(port: u16) -> bool {
let url = format!("http://127.0.0.1:{}/v1/models", port);
reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(1))
.build()
.ok()
.and_then(|client| client.get(&url).send().ok())
.map(|response| response.status().is_success())
.unwrap_or(false)
}
fn parse_session_result_from_stdout(text: &str) -> Option<crate::cli::headless::SessionResult> {
for line in text
.lines()
.rev()
.map(|l| l.trim())
.filter(|l| !l.is_empty())
{
if let Ok(result) = serde_json::from_str::<crate::cli::headless::SessionResult>(line) {
return Some(result);
}
}
None
}
pub fn run_selfware(
selfware_bin: &Path,
workdir: &Path,
prompt: &str,
alias: &str,
endpoint: &str,
timeout: Duration,
log_path: &Path,
result_dir: &Path,
) -> Result<RunOutcome> {
std::fs::create_dir_all(result_dir)
.with_context(|| format!("creating result dir {}", result_dir.display()))?;
let result_dir = if result_dir.is_absolute() {
result_dir.to_path_buf()
} else {
std::env::current_dir()?.join(result_dir)
};
let log = std::fs::File::create(log_path)
.with_context(|| format!("opening agent log {}", log_path.display()))?;
let bench_config_path = result_dir.join("selfware_bench.toml");
write_bench_selfware_config(&bench_config_path, endpoint, alias)
.with_context(|| format!("writing {}", bench_config_path.display()))?;
let mut cmd = Command::new(selfware_bin);
apply_env_allowlist(&mut cmd);
cmd.arg("-p")
.arg(prompt)
.arg("-C")
.arg(workdir)
.arg("--yolo")
.arg("--no-tui")
.arg("--quiet")
.arg("--output-format")
.arg("json")
.env("SELFWARE_CONFIG", &bench_config_path)
.env("SELFWARE_ENDPOINT", endpoint)
.env("SELFWARE_MODEL", alias)
.env("SELFWARE_RESULT_DIR", &result_dir)
.stdin(Stdio::null())
.stderr(Stdio::from(log));
let outcome = run_with_group_timeout(cmd, timeout)?;
if outcome.timed_out {
return Ok(RunOutcome {
exit_code: -1,
timed_out: true,
wall_secs: outcome.wall_secs,
parsed_result: None,
});
}
if let Some(result) = parse_session_result_from_stdout(&outcome.stdout) {
return Ok(RunOutcome {
exit_code: result.exit_status,
timed_out: false,
wall_secs: result.duration_ms as f64 / 1000.0,
parsed_result: Some(result),
});
}
Ok(RunOutcome {
exit_code: outcome.exit_code,
timed_out: false,
wall_secs: outcome.wall_secs,
parsed_result: None,
})
}
fn toml_string(value: &str) -> String {
serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
}
fn write_bench_selfware_config(path: &Path, endpoint: &str, alias: &str) -> Result<()> {
let content = format!(
"endpoint = {}\n\
model = {}\n\
temperature = 0.7\n\
max_tokens = 32768\n\n\
[agent]\n\
native_function_calling = false\n\
streaming = true\n\
read_loop_policy = \"force_mutation\"\n\
disable_turn_artifacts = false\n",
toml_string(endpoint),
toml_string(alias)
);
std::fs::write(path, content)?;
Ok(())
}
#[derive(Clone, Debug)]
pub struct RunOutcome {
pub exit_code: i32,
pub timed_out: bool,
pub wall_secs: f64,
pub parsed_result: Option<crate::cli::headless::SessionResult>,
}
pub fn capture_patch(workdir: &Path) -> Result<String> {
let tmp_index = workdir
.join(".git")
.join(format!("selfware-tmp-index-{}", std::process::id()));
let _ = Command::new("git")
.env("GIT_INDEX_FILE", &tmp_index)
.args(["-C"])
.arg(workdir)
.args(["add", "-A"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
let out = Command::new("git")
.env("GIT_INDEX_FILE", &tmp_index)
.args(["-C"])
.arg(workdir)
.args([
"diff",
"--cached",
"HEAD",
"--",
".",
":(exclude).selfware/**",
":(exclude).claude/**",
":(exclude)__pycache__/**",
":(exclude)**/__pycache__/**",
":(exclude)selfware.toml",
":(exclude)reports/swebench_pro/**",
":(exclude)trace.jsonl",
":(exclude)**/trace.jsonl",
":(exclude)failure_mode.json",
":(exclude)**/failure_mode.json",
":(exclude)*.bak",
":(exclude)**/*.bak",
])
.output()
.with_context(|| format!("running git diff in {}", workdir.display()))?;
let _ = std::fs::remove_file(&tmp_index);
if !out.status.success() {
bail!(
"git diff failed (status={:?}): {}",
out.status.code(),
String::from_utf8_lossy(&out.stderr)
);
}
String::from_utf8(out.stdout).map_err(|e| anyhow!("non-UTF8 patch: {}", e))
}
pub fn clone_instance(repo: &str, base_commit: &str, dest: &Path) -> Result<()> {
if dest.exists() {
std::fs::remove_dir_all(dest)
.with_context(|| format!("removing existing dest {}", dest.display()))?;
}
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("creating {}", parent.display()))?;
}
let url = format!("https://github.com/{}.git", repo);
let init = Command::new("git")
.arg("init")
.arg(dest)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.with_context(|| "git init")?;
if init.success() {
let add_remote = Command::new("git")
.args(["-C"])
.arg(dest)
.args(["remote", "add", "origin", &url])
.status()?;
let fetch = Command::new("git")
.args(["-C"])
.arg(dest)
.args(["fetch", "--depth", "1", "origin", base_commit])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
let checkout = Command::new("git")
.args(["-C"])
.arg(dest)
.args(["checkout", "FETCH_HEAD"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()?;
if add_remote.success() && fetch.success() && checkout.success() {
return Ok(());
}
}
if dest.exists() {
let _ = std::fs::remove_dir_all(dest);
}
let clone_status = Command::new("git")
.args(["clone", "--filter=blob:none", "--depth", "200", &url])
.arg(dest)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.with_context(|| "git clone fallback")?;
if !clone_status.success() {
bail!(
"git clone fallback failed for {} (status={:?})",
url,
clone_status.code()
);
}
let checkout_status = Command::new("git")
.args(["-C"])
.arg(dest)
.args(["checkout", base_commit])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.with_context(|| "git checkout fallback")?;
if !checkout_status.success() {
bail!(
"git checkout fallback failed for {}@{} (status={:?})",
url,
base_commit,
checkout_status.code()
);
}
Ok(())
}
#[cfg(test)]
#[path = "../../../tests/unit/bench_harness/swebench_pro/harness/harness_test.rs"]
mod tests;