use anyhow::{bail, Context, Result};
use std::time::Duration;
use crate::harness::{Harness, Runner};
use crate::tmux;
pub fn parse_duration(s: &str) -> Result<Duration> {
let s = s.trim().to_lowercase();
if s.ends_with('m') {
let mins: u64 = s
.trim_end_matches('m')
.parse()
.context("Invalid minutes value")?;
Ok(Duration::from_secs(mins * 60))
} else if s.ends_with('h') {
let hours: u64 = s
.trim_end_matches('h')
.parse()
.context("Invalid hours value")?;
Ok(Duration::from_secs(hours * 3600))
} else if s.ends_with('s') {
let secs: u64 = s
.trim_end_matches('s')
.parse()
.context("Invalid seconds value")?;
Ok(Duration::from_secs(secs))
} else {
let mins: u64 = s.parse().context("Invalid duration value")?;
Ok(Duration::from_secs(mins * 60))
}
}
pub struct MonitorConfig {
pub monitor_harness: Harness,
pub monitor_model: String,
pub monitor_interval: Duration,
pub inner_harness: Harness,
pub inner_model: String,
pub task: String,
pub dangerous: bool,
pub reasoning_effort: String,
pub provider: String,
}
pub async fn run_monitor(config: MonitorConfig) -> Result<()> {
if !tmux::tmux_available() {
bail!("Monitor mode requires tmux. Please install tmux first.");
}
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let base_inner_session = format!("ralph-inner-{}", timestamp);
let inner_session = tmux::unique_session_name(&base_inner_session);
if inner_session != base_inner_session {
eprintln!(
"Session name '{}' already exists, using '{}'",
base_inner_session, inner_session
);
}
println!(
"Starting inner agent ({}) in tmux session: {}",
config.inner_harness.command_name(),
inner_session
);
let inner_cmd = build_ralph_command(
&config.inner_harness,
&config.inner_model,
config.dangerous,
&config.reasoning_effort,
&config.provider,
&config.task,
"inf", );
tmux::start_in_tmux(&inner_session, "ralph", &inner_cmd, false)?;
println!("Inner agent started. Session: {}", inner_session);
println!("Monitor will check every {:?}", config.monitor_interval);
println!();
let monitor_runner = Runner::new(
config.monitor_harness,
config.monitor_model,
config.dangerous,
config.reasoning_effort,
config.provider,
);
loop {
tokio::time::sleep(config.monitor_interval).await;
if !tmux::session_exists(&inner_session) {
println!("Inner session {} has ended.", inner_session);
break;
}
let output = tmux::capture_tmux_output(&inner_session, 100)?;
let monitor_prompt = create_monitor_prompt(&config.task, &output);
println!("--- Monitor Check ---");
println!("Checking inner agent progress...");
if let Err(e) = monitor_runner.run(&monitor_prompt).await {
eprintln!("Monitor agent error: {}", e);
}
println!("--- End Monitor Check ---\n");
}
Ok(())
}
fn build_ralph_command(
harness: &Harness,
model: &str,
dangerous: bool,
reasoning: &str,
provider: &str,
task: &str,
iterations: &str,
) -> Vec<String> {
let mut args = vec![
"-H".to_string(),
harness.command_name().to_string(),
"-m".to_string(),
model.to_string(),
"-n".to_string(),
iterations.to_string(),
"--reasoning".to_string(),
reasoning.to_string(),
];
if dangerous {
args.push("--dangerous".to_string());
} else {
args.push("--safe".to_string());
}
if !provider.is_empty() {
args.push("--provider".to_string());
args.push(provider.to_string());
}
args.push("--no-tmux".to_string()); args.push(task.to_string());
args
}
fn create_monitor_prompt(original_task: &str, inner_output: &str) -> String {
format!(
r#"You are a monitor agent watching an inner coding agent work on a task.
ORIGINAL TASK:
{}
RECENT OUTPUT FROM INNER AGENT (last 100 lines):
{}
Your job:
1. Assess if the inner agent is making progress on the task
2. Check if it seems stuck or in an error loop
3. If needed, you can update TASK.md to provide guidance or corrections
4. Report your observations
What is the current status of the inner agent's work?"#,
original_task, inner_output
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_duration_minutes() {
assert_eq!(parse_duration("5m").unwrap(), Duration::from_secs(300));
assert_eq!(parse_duration("1m").unwrap(), Duration::from_secs(60));
assert_eq!(parse_duration("10m").unwrap(), Duration::from_secs(600));
}
#[test]
fn test_parse_duration_hours() {
assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600));
assert_eq!(parse_duration("2h").unwrap(), Duration::from_secs(7200));
}
#[test]
fn test_parse_duration_seconds() {
assert_eq!(parse_duration("30s").unwrap(), Duration::from_secs(30));
assert_eq!(parse_duration("90s").unwrap(), Duration::from_secs(90));
}
#[test]
fn test_parse_duration_no_suffix() {
assert_eq!(parse_duration("5").unwrap(), Duration::from_secs(300));
}
#[test]
fn test_parse_duration_invalid() {
assert!(parse_duration("abc").is_err());
assert!(parse_duration("").is_err());
}
#[test]
fn test_build_ralph_command() {
let args = build_ralph_command(
&Harness::Claude,
"claude-opus",
true,
"medium",
"",
"TASK.md",
"5",
);
assert!(args.contains(&"-H".to_string()));
assert!(args.contains(&"claude".to_string()));
assert!(args.contains(&"-m".to_string()));
assert!(args.contains(&"claude-opus".to_string()));
assert!(args.contains(&"--dangerous".to_string()));
assert!(args.contains(&"--no-tmux".to_string()));
assert!(args.contains(&"TASK.md".to_string()));
}
}