use std::path::Path;
use serde_json::Value;
use super::records::{DRIVER_LOG_FILE, STDERR_FILE};
use super::{
INPUT_FILE, META_FILE, OUTPUT_FILE, REQUEST_FILE, RESPONSE_FILE, STAGING_FILE, STEPS_DIR,
TOOLS_SUBDIR,
};
use crate::files_view::Preview;
pub const UNPARSED: &str = "unparseable JSON — bytes verbatim below";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Doc {
Json { value: Value, raw: Vec<u8> },
Absent,
Unparsed(Vec<u8>),
}
impl Doc {
pub(super) fn of_bytes(bytes: Vec<u8>) -> Doc {
if bytes.is_empty() {
return Doc::Absent;
}
match serde_json::from_slice(&bytes) {
Ok(value) => Doc::Json { value, raw: bytes },
Err(_) => Doc::Unparsed(bytes),
}
}
fn of_file(path: &Path) -> Doc {
Doc::of_bytes(std::fs::read(path).unwrap_or_default())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolIo {
pub tool_id: String,
pub input: Doc,
pub output: Doc,
pub is_error: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StepDetail {
pub seq: String,
pub meta: Doc,
pub request: Doc,
pub staging: Doc,
pub response: Vec<Doc>,
pub tools: Vec<ToolIo>,
pub stderr: Option<Preview>,
pub driver: Option<Preview>,
}
pub fn detail(workspace: &Path, agent_id: &str, seq: &str) -> Result<StepDetail, String> {
let seq = super::seq::resolve(workspace, agent_id, seq)?;
let agent = workspace.join(STEPS_DIR).join(agent_id);
let step = agent.join(&seq);
Ok(StepDetail {
seq,
meta: Doc::of_file(&step.join(META_FILE)),
request: Doc::of_file(&step.join(REQUEST_FILE)),
staging: Doc::of_file(&step.join(STAGING_FILE)),
response: response_events(&step.join(RESPONSE_FILE)),
tools: tool_ios(&step.join(TOOLS_SUBDIR)),
stderr: log(&step.join(STDERR_FILE)),
driver: log(&agent.join(DRIVER_LOG_FILE)),
})
}
fn log(path: &Path) -> Option<Preview> {
let size = std::fs::symlink_metadata(path).ok()?.len();
(size > 0).then(|| crate::files_view::preview(path))
}
fn response_events(path: &Path) -> Vec<Doc> {
let Ok(bytes) = std::fs::read(path) else {
return Vec::new();
};
bytes
.split(|&b| b == b'\n')
.filter(|line| !line.is_empty())
.map(|line| Doc::of_bytes(line.to_vec()))
.collect()
}
fn tool_ios(tools_dir: &Path) -> Vec<ToolIo> {
let Ok(entries) = std::fs::read_dir(tools_dir) else {
return Vec::new();
};
let mut tools: Vec<ToolIo> = entries
.flatten()
.filter_map(|entry| {
let path = entry.path();
if !path.is_dir() {
return None;
}
let tool_id = entry.file_name().to_str()?.to_string();
let output = Doc::of_file(&path.join(OUTPUT_FILE));
let is_error = output_is_error(&output);
Some(ToolIo {
tool_id,
input: Doc::of_file(&path.join(INPUT_FILE)),
output,
is_error,
})
})
.collect();
tools.sort_by(|a, b| a.tool_id.cmp(&b.tool_id));
tools
}
fn output_is_error(output: &Doc) -> bool {
match output {
Doc::Json { value, .. } => value
.get("exit_code")
.and_then(Value::as_i64)
.is_some_and(|code| code != 0),
Doc::Absent | Doc::Unparsed(_) => false,
}
}