#[derive(Debug, Clone)]
pub struct ToolRecord {
pub tool_call_id: String,
pub tool_name: String,
pub full_output: String,
pub summary: String,
}
#[derive(Default)]
pub struct History {
records: Vec<ToolRecord>,
}
impl History {
pub fn record(&mut self, tool_call_id: &str, tool_name: &str, full_output: &str) -> (String, String) {
let model_output = summarise(tool_name, full_output);
let display_summary = display_summarise(tool_name, full_output);
self.records.push(ToolRecord {
tool_call_id: tool_call_id.to_string(),
tool_name: tool_name.to_string(),
full_output: full_output.to_string(),
summary: model_output.clone(),
});
(model_output, display_summary)
}
pub fn recall(&self, tool_call_id: &str) -> Option<&str> {
self.records
.iter()
.find(|r| r.tool_call_id == tool_call_id)
.map(|r| r.full_output.as_str())
}
pub fn recall_by_name(&self, tool_name: &str) -> Option<&str> {
self.records
.iter()
.rfind(|r| r.tool_name == tool_name)
.map(|r| r.full_output.as_str())
}
pub fn compressed_count(&self) -> usize {
self.records
.iter()
.filter(|r| r.summary.len() < r.full_output.len())
.count()
}
pub fn compress_reads_for(&mut self, path: &str) {
for rec in &mut self.records {
if rec.tool_name == "read_file" && rec.summary.contains(path) && rec.summary.len() > 200 {
rec.summary = format!(
"[Previously read {path} — content is now stale after edit. \
Use read_file to get current content if needed.]"
);
}
}
}
}
fn display_summarise(tool_name: &str, output: &str) -> String {
match tool_name {
"read_file" => {
let first = first_line(output);
if first.starts_with('[') {
let inner = first.trim_start_matches('[');
let path_part = inner
.split(" —")
.next()
.unwrap_or(inner)
.trim_end_matches(']');
let content_lines = output.lines().filter(|l| l.contains(" | ")).count();
if content_lines > 0 {
return format!("✓ Read {path_part} ({content_lines} lines shown)");
}
return format!("✓ Read {path_part}");
}
format!("✓ Read file ({} lines)", output.lines().count())
}
_ => summarise(tool_name, output),
}
}
fn summarise(tool_name: &str, output: &str) -> String {
match tool_name {
"read_file" => output.to_string(),
"write_file" | "edit_file" => {
if output.contains("⚠ FILE WRITTEN BUT BUILD BROKEN") || output.contains("✗ build check failed") {
output.to_string()
} else {
output.to_string()
}
}
"list_files" => summarise_list(output),
"search" => summarise_search(output),
"bash" => summarise_bash(output),
_ => truncate_to_lines(output, 3),
}
}
fn summarise_list(output: &str) -> String {
if let Some(last) = output.lines().last() {
if last.starts_with('[') {
let path = output
.lines()
.next()
.and_then(|l| l.split_whitespace().next())
.unwrap_or(".");
return format!("✓ Listed {path}: {}", last.trim_start_matches('[').trim_end_matches(']'));
}
}
let count = output.lines().filter(|l| l.contains("──")).count();
format!("✓ Listed directory ({count} entries)")
}
fn summarise_search(output: &str) -> String {
if output.starts_with("No matches") {
return output.lines().next().unwrap_or("No matches").to_string();
}
let match_lines: Vec<&str> = output
.lines()
.filter(|l| {
let parts: Vec<&str> = l.splitn(3, ':').collect();
parts.len() >= 2 && parts[1].parse::<u32>().is_ok()
})
.collect();
let n = match_lines.len();
if n == 0 {
return truncate_to_lines(output, 2);
}
let mut locations: Vec<String> = match_lines
.iter()
.filter_map(|l| {
let mut parts = l.splitn(3, ':');
let file = parts.next()?;
let line = parts.next()?;
Some(format!("{file}:{line}"))
})
.collect::<std::collections::LinkedList<_>>() .into_iter()
.collect::<Vec<_>>();
locations.dedup();
let shown: Vec<&str> = locations.iter().take(5).map(String::as_str).collect();
let tail = if locations.len() > 5 {
format!(", +{} more", locations.len() - 5)
} else {
String::new()
};
format!("✓ search → {n} matches: {}{tail}", shown.join(", "))
}
fn summarise_bash(output: &str) -> String {
const MAX_SUMMARY: usize = 25;
const MAX_ERROR_LINES: usize = 20;
const SUCCESS_HEAD: usize = 5;
let lines: Vec<&str> = output.lines().collect();
if lines.len() <= SUCCESS_HEAD {
return output.to_string();
}
let error_lines: Vec<(usize, &&str)> = lines.iter().enumerate()
.filter(|(_, l)| {
let l = l.to_ascii_lowercase();
l.contains("error:") || l.contains("error[")
|| l.contains("failed") || l.contains("fail:")
|| l.contains("panic") || l.contains("warning:")
|| l.contains("cannot") || l.contains("note:")
})
.collect();
if error_lines.is_empty() {
let head = lines[..SUCCESS_HEAD].join("\n");
return format!("{head}\n[+{} lines — full output stored, ask to recall]", lines.len() - SUCCESS_HEAD);
}
let kept: Vec<&str> = error_lines.iter()
.take(MAX_ERROR_LINES)
.map(|(_, l)| **l)
.collect();
let shown = kept.len().min(MAX_SUMMARY);
let result = kept[..shown].join("\n");
let remaining = lines.len().saturating_sub(shown);
if remaining > 0 {
format!("{result}\n[+{remaining} lines — full output stored, ask to recall]")
} else {
result
}
}
fn first_line(s: &str) -> &str {
s.lines().next().unwrap_or(s)
}
fn truncate_to_lines(s: &str, n: usize) -> String {
let lines: Vec<&str> = s.lines().collect();
if lines.len() <= n {
return s.to_string();
}
format!("{}\n[+{} lines truncated]", lines[..n].join("\n"), lines.len() - n)
}
#[test]
fn test_history_record() {
let mut history = History::default();
let (summary, display) = history.record("test_id", "test_tool", "test_output");
assert_eq!(summary, "test_output");
assert_eq!(display, "test_output");
assert_eq!(history.records.len(), 1);
}
#[test]
fn test_history_recall() {
let mut history = History::default();
history.records.push(ToolRecord {
tool_call_id: "test_id".to_string(),
tool_name: "test_tool".to_string(),
full_output: "test_output".to_string(),
summary: "summary".to_string(),
});
assert_eq!(history.recall("test_id"), Some("test_output"));
}
#[test]
fn test_history_recall_by_name() {
let mut history = History::default();
history.records.push(ToolRecord {
tool_call_id: "test_id".to_string(),
tool_name: "test_tool".to_string(),
full_output: "test_output".to_string(),
summary: "summary".to_string(),
});
assert_eq!(history.recall_by_name("test_tool"), Some("test_output"));
}
#[test]
fn test_history_compressed_count() {
let mut history = History::default();
history.records.push(ToolRecord {
tool_call_id: "test_id".to_string(),
tool_name: "test_tool".to_string(),
full_output: "test_output".to_string(),
summary: "summary".to_string(),
});
assert_eq!(history.compressed_count(), 1);
}