use crate::git;
use std::io::Write;
use std::process::{Command, Stdio};
pub struct Answer {
pub text: String,
pub reviewer: String,
pub session: String,
}
#[derive(Clone, Copy)]
pub enum Role {
Review,
JudgeIntent,
}
pub struct Agent;
impl Agent {
pub fn named(name: &str) -> Result<Agent, String> {
if name.trim() != "claude" {
return Err(format!(
"unknown agent '{}': this build knows claude.\n git config --global agent-verdict.runner claude",
name.trim()
));
}
Ok(Agent)
}
pub fn run(
&self,
role: Role,
system: &str,
prompt: &str,
resume: Option<&str>,
) -> Result<Answer, String> {
claude(role, system, prompt, resume)
}
}
fn system_file(text: &str) -> Result<std::path::PathBuf, String> {
let path = git::git_path("AGENT_VERDICT_SYSTEM")?;
std::fs::write(&path, text).map_err(|e| format!("cannot write {}: {e}", path.display()))?;
Ok(path)
}
fn claude_model(role: Role) -> Option<&'static str> {
match role {
Role::Review => None,
Role::JudgeIntent => Some("haiku"),
}
}
fn claude(role: Role, system: &str, prompt: &str, resume: Option<&str>) -> Result<Answer, String> {
let file = system_file(system)?;
let mut command = Command::new("claude");
command.args(["-p", "--output-format", "json"]);
command.arg("--append-system-prompt-file").arg(&file);
if let Some(session) = resume {
command.args(["--resume", session]);
}
if let Some(model) = claude_model(role) {
command.args(["--model", model]);
}
let out = piped(command, prompt)?;
let _ = std::fs::remove_file(&file);
read_claude(&out)
}
fn piped(mut command: Command, prompt: &str) -> Result<String, String> {
let mut child = command
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.map_err(|e| format!("cannot run the reviewer: {e}"))?;
let mut stdin = child.stdin.take().ok_or("the reviewer took no stdin")?;
let text = prompt.to_string();
let writer = std::thread::spawn(move || stdin.write_all(text.as_bytes()));
let out = child
.wait_with_output()
.map_err(|e| format!("the reviewer did not finish: {e}"))?;
match writer.join() {
Err(_) => return Err("the prompt was never written to the reviewer".to_string()),
Ok(Err(e)) if e.kind() != std::io::ErrorKind::BrokenPipe => {
return Err(format!("cannot brief the reviewer: {e}"));
}
Ok(_) => {}
}
if !out.status.success() {
return Err(format!("the reviewer exited {}", out.status));
}
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
}
fn read_claude(out: &str) -> Result<Answer, String> {
let json: serde_json::Value =
serde_json::from_str(out).map_err(|e| format!("the reviewer's answer is not JSON: {e}"))?;
if json["is_error"].as_bool().unwrap_or(false) {
let said = json["result"].as_str().unwrap_or("no reason given");
return Err(format!("the reviewer reported an error: {said}"));
}
let text = json["result"]
.as_str()
.ok_or("the reviewer's answer carries no result")?;
let session = json["session_id"]
.as_str()
.ok_or("the reviewer's answer carries no session_id")?;
let reviewer = json["modelUsage"]
.as_object()
.and_then(|used| used.keys().next().cloned())
.unwrap_or_else(|| "claude".to_string());
Ok(Answer {
text: text.to_string(),
reviewer,
session: session.to_string(),
})
}