use std::path::Path;
use super::streaming::latest_step_dir;
use super::{STEPS_DIR, ToolCall, ToolCallState};
const TOOLS_SUBDIR: &str = "tools";
const INPUT_FILE: &str = "input.json";
const OUTPUT_FILE: &str = "output.json";
pub(super) fn tool_calls_from_disk(workspace: &Path, agent_id: &str) -> Vec<ToolCall> {
let agent_steps = workspace.join(STEPS_DIR).join(agent_id);
let Some(latest) = latest_step_dir(&agent_steps) else {
return Vec::new();
};
let tools_dir = latest.join(TOOLS_SUBDIR);
let Ok(entries) = std::fs::read_dir(&tools_dir) else {
return Vec::new();
};
let mut calls: Vec<ToolCall> = 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();
if !path.join(INPUT_FILE).exists() {
return None;
}
let state = if path.join(OUTPUT_FILE).exists() {
ToolCallState::Complete
} else {
ToolCallState::InFlight
};
let input = path.join(INPUT_FILE);
let name = tool_name(&input);
let start_unix = super::enumerate::mtime_unix(&input);
Some(ToolCall {
tool_id,
name,
start_unix,
state,
})
})
.collect();
calls.sort_by(|a, b| a.tool_id.cmp(&b.tool_id));
calls
}
fn tool_name(input: &Path) -> Option<String> {
let bytes = std::fs::read(input).ok()?;
let value: serde_json::Value = serde_json::from_slice(&bytes).ok()?;
Some(value.get("name")?.as_str()?.to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
fn write(path: &std::path::PathBuf, contents: &[u8]) {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
std::fs::write(path, contents).unwrap();
}
fn tool_dir(root: &Path, conv: &str, seq: u32, tool_id: &str) -> std::path::PathBuf {
root.join(STEPS_DIR)
.join(conv)
.join(format!("{seq:03}"))
.join(TOOLS_SUBDIR)
.join(tool_id)
}
#[test]
fn input_only_yields_in_flight() {
let dir = tempdir().unwrap();
let conv = "20260427T130000Z-aaaa";
let t = tool_dir(dir.path(), conv, 1, "toolu_01a");
write(&t.join(INPUT_FILE), br#"{"name":"Read","input":{}}"#);
let calls = tool_calls_from_disk(dir.path(), conv);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].tool_id, "toolu_01a");
assert_eq!(calls[0].state, ToolCallState::InFlight);
assert_eq!(calls[0].name.as_deref(), Some("Read"));
}
#[test]
fn a_nameless_or_malformed_record_yields_no_name() {
let dir = tempdir().unwrap();
let conv = "20260427T130000Z-nnnn";
write(
&tool_dir(dir.path(), conv, 1, "toolu_a").join(INPUT_FILE),
b"{}",
);
write(
&tool_dir(dir.path(), conv, 1, "toolu_b").join(INPUT_FILE),
b"{partial",
);
write(
&tool_dir(dir.path(), conv, 1, "toolu_c").join(INPUT_FILE),
br#"{"name":7}"#,
);
let calls = tool_calls_from_disk(dir.path(), conv);
assert_eq!(calls.len(), 3);
assert!(calls.iter().all(|c| c.name.is_none()));
}
#[test]
fn input_and_output_yield_complete() {
let dir = tempdir().unwrap();
let conv = "20260427T130000Z-bbbb";
let t = tool_dir(dir.path(), conv, 1, "toolu_01b");
write(&t.join(INPUT_FILE), b"{}");
write(&t.join(OUTPUT_FILE), b"{}");
let calls = tool_calls_from_disk(dir.path(), conv);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].state, ToolCallState::Complete);
}
#[test]
fn returns_empty_when_steps_dir_absent() {
let dir = tempdir().unwrap();
assert!(tool_calls_from_disk(dir.path(), "no-such-conv").is_empty());
}
#[test]
fn returns_empty_when_tools_dir_absent() {
let dir = tempdir().unwrap();
let conv = "20260427T130000Z-cccc";
std::fs::create_dir_all(dir.path().join(STEPS_DIR).join(conv).join("001")).unwrap();
assert!(tool_calls_from_disk(dir.path(), conv).is_empty());
}
#[test]
fn skips_entry_without_input_json() {
let dir = tempdir().unwrap();
let conv = "20260427T130000Z-dddd";
let t = tool_dir(dir.path(), conv, 1, "toolu_01d");
std::fs::create_dir_all(&t).unwrap();
assert!(tool_calls_from_disk(dir.path(), conv).is_empty());
}
#[test]
fn skips_non_directory_entries() {
let dir = tempdir().unwrap();
let conv = "20260427T130000Z-eeee";
let tools = dir
.path()
.join(STEPS_DIR)
.join(conv)
.join("001")
.join(TOOLS_SUBDIR);
std::fs::create_dir_all(&tools).unwrap();
write(&tools.join(".keep"), b"");
let real = tools.join("toolu_01e");
std::fs::create_dir_all(&real).unwrap();
write(&real.join(INPUT_FILE), b"{}");
let calls = tool_calls_from_disk(dir.path(), conv);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].tool_id, "toolu_01e");
}
#[test]
fn reads_only_latest_step_tools() {
let dir = tempdir().unwrap();
let conv = "20260427T130000Z-ffff";
let old = tool_dir(dir.path(), conv, 1, "toolu_old");
write(&old.join(INPUT_FILE), b"{}");
write(&old.join(OUTPUT_FILE), b"{}");
let new_inflight = tool_dir(dir.path(), conv, 2, "toolu_new");
write(&new_inflight.join(INPUT_FILE), b"{}");
let calls = tool_calls_from_disk(dir.path(), conv);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].tool_id, "toolu_new");
assert_eq!(calls[0].state, ToolCallState::InFlight);
}
#[test]
fn sorts_calls_by_tool_id() {
let dir = tempdir().unwrap();
let conv = "20260427T130000Z-gggg";
for id in ["toolu_03", "toolu_01", "toolu_02"] {
let t = tool_dir(dir.path(), conv, 1, id);
write(&t.join(INPUT_FILE), b"{}");
}
let calls = tool_calls_from_disk(dir.path(), conv);
let ids: Vec<&str> = calls.iter().map(|c| c.tool_id.as_str()).collect();
assert_eq!(ids, vec!["toolu_01", "toolu_02", "toolu_03"]);
}
}