use std::path::Path;
use serde_json::Value;
use super::{
INPUT_FILE, META_FILE, OUTPUT_FILE, REQUEST_FILE, RESPONSE_FILE, STAGING_FILE, STEPS_DIR,
TOOLS_SUBDIR,
};
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),
}
}
pub(super) fn raw(&self) -> &[u8] {
match self {
Doc::Json { raw, .. } | Doc::Unparsed(raw) => raw,
Doc::Absent => &[],
}
}
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 fn detail(workspace: &Path, agent_id: &str, seq: &str) -> StepDetail {
let step = workspace.join(STEPS_DIR).join(agent_id).join(seq);
StepDetail {
seq: seq.to_string(),
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)),
}
}
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,
}
}