use super::run_id::RunId;
use crate::workspace::Workspace;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
pub struct RunLogContext {
run_id: RunId,
run_dir: PathBuf,
}
impl RunLogContext {
pub fn new(workspace: &dyn Workspace) -> Result<Self> {
let base_run_id = RunId::new();
let (run_id, run_dir) = crate::logging::collision::create_run_dir_with_collision_handling(
workspace,
&base_run_id,
)?;
Ok(Self { run_id, run_dir })
}
pub fn from_checkpoint(run_id: &str, workspace: &dyn Workspace) -> Result<Self> {
let run_id = RunId::from_checkpoint(run_id);
let run_dir = PathBuf::from(format!(".agent/logs-{run_id}"));
if !workspace.exists(&run_dir) {
workspace
.create_dir_all(&run_dir)
.context("Failed to recreate run log directory for resume")?;
workspace
.create_dir_all(&run_dir.join("agents"))
.context("Failed to recreate agents log subdirectory for resume")?;
workspace
.create_dir_all(&run_dir.join("provider"))
.context("Failed to recreate provider log subdirectory for resume")?;
workspace
.create_dir_all(&run_dir.join("debug"))
.context("Failed to recreate debug log subdirectory for resume")?;
}
Ok(Self { run_id, run_dir })
}
pub fn for_testing(base_run_id: &RunId, workspace: &dyn Workspace) -> Result<Self> {
let (run_id, run_dir) = crate::logging::collision::create_run_dir_with_collision_handling(
workspace,
base_run_id,
)?;
Ok(Self { run_id, run_dir })
}
#[must_use]
pub const fn run_id(&self) -> &RunId {
&self.run_id
}
#[must_use]
pub fn run_dir(&self) -> &Path {
&self.run_dir
}
#[must_use]
pub fn pipeline_log(&self) -> PathBuf {
self.run_dir.join("pipeline.log")
}
#[must_use]
pub fn event_loop_log(&self) -> PathBuf {
self.run_dir.join("event_loop.log")
}
#[must_use]
pub fn event_loop_trace(&self) -> PathBuf {
self.run_dir.join("event_loop_trace.jsonl")
}
#[must_use]
pub fn agent_log(&self, phase: &str, index: u32, attempt: Option<u32>) -> PathBuf {
let filename = attempt.map_or_else(
|| format!("{phase}_{index}.log"),
|a| format!("{phase}_{index}_a{a}.log"),
);
self.run_dir.join("agents").join(filename)
}
#[must_use]
pub fn provider_log(&self, name: &str) -> PathBuf {
self.run_dir.join("provider").join(name)
}
#[must_use]
pub fn run_metadata(&self) -> PathBuf {
self.run_dir.join("run.json")
}
pub fn write_run_metadata(
&self,
workspace: &dyn Workspace,
metadata: &RunMetadata,
) -> Result<()> {
let path = self.run_metadata();
let json = serde_json::to_string_pretty(metadata).with_context(|| {
format!(
"Failed to serialize run metadata for run_id '{}'. \
This usually means a field contains data that cannot be represented as JSON.",
self.run_id
)
})?;
workspace.write(&path, &json).with_context(|| {
format!(
"Failed to write run.json to '{}'. Check filesystem permissions and disk space.",
path.display()
)
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunMetadata {
pub run_id: String,
pub started_at_utc: String,
pub command: String,
pub resume: bool,
pub repo_root: String,
pub ralph_version: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config_summary: Option<ConfigSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigSummary {
#[serde(skip_serializing_if = "Option::is_none")]
pub developer_agent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reviewer_agent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_iterations: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_reviewer_passes: Option<u32>,
}
#[cfg(test)]
mod tests;