use crate::providers::ChatMessage;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::path::Path;
pub(crate) const DEFAULT_MAX_HISTORY_MESSAGES: usize = 50;
pub(crate) fn floor_char_boundary(s: &str, i: usize) -> usize {
if i >= s.len() {
return s.len();
}
let mut pos = i;
while pos > 0 && !s.is_char_boundary(pos) {
pos -= 1;
}
pos
}
pub(crate) fn truncate_tool_result(output: &str, max_chars: usize) -> String {
if max_chars == 0 || output.len() <= max_chars {
return output.to_string();
}
let head_len = max_chars * 2 / 3;
let tail_len = max_chars.saturating_sub(head_len);
let head_end = floor_char_boundary(output, head_len);
let tail_start_raw = output.len().saturating_sub(tail_len);
let tail_start = if tail_start_raw >= output.len() {
output.len()
} else {
let mut pos = tail_start_raw;
while pos < output.len() && !output.is_char_boundary(pos) {
pos += 1;
}
pos
};
if head_end >= tail_start {
return output[..floor_char_boundary(output, max_chars)].to_string();
}
let truncated_chars = tail_start - head_end;
format!(
"{}\n\n[... {} characters truncated ...]\n\n{}",
&output[..head_end],
truncated_chars,
&output[tail_start..]
)
}
pub(crate) fn truncate_tool_message(msg_content: &str, max_chars: usize) -> String {
if max_chars == 0 || msg_content.len() <= max_chars {
return msg_content.to_string();
}
if let Ok(mut obj) =
serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(msg_content)
{
if obj.contains_key("tool_call_id") {
if let Some(serde_json::Value::String(inner)) = obj.get("content") {
let truncated = truncate_tool_result(inner, max_chars);
obj.insert("content".to_string(), serde_json::Value::String(truncated));
return serde_json::to_string(&obj).unwrap_or_else(|_| msg_content.to_string());
}
}
}
truncate_tool_result(msg_content, max_chars)
}
pub(crate) fn fast_trim_tool_results(
history: &mut [crate::providers::ChatMessage],
protect_last_n: usize,
) -> usize {
let trim_to = 2000;
let mut saved = 0;
let cutoff = history.len().saturating_sub(protect_last_n);
for msg in &mut history[..cutoff] {
if msg.role == "tool" && msg.content.len() > trim_to {
let original_len = msg.content.len();
msg.content = truncate_tool_message(&msg.content, trim_to);
saved += original_len - msg.content.len();
}
}
saved
}
pub(crate) fn emergency_history_trim(
history: &mut Vec<crate::providers::ChatMessage>,
keep_recent: usize,
) -> usize {
let mut dropped = 0;
let target_drop = history.len() / 3;
let mut i = 0;
while dropped < target_drop && i < history.len().saturating_sub(keep_recent) {
if history[i].role == "system" {
i += 1;
} else {
history.remove(i);
dropped += 1;
}
}
dropped
}
pub(crate) fn estimate_history_tokens(history: &[ChatMessage]) -> usize {
history
.iter()
.map(|m| {
m.content.len().div_ceil(4) + 4
})
.sum()
}
pub(crate) fn trim_history(history: &mut Vec<ChatMessage>, max_history: usize) {
let has_system = history.first().map_or(false, |m| m.role == "system");
let non_system_count = if has_system {
history.len() - 1
} else {
history.len()
};
if non_system_count <= max_history {
return;
}
let start = if has_system { 1 } else { 0 };
let to_remove = non_system_count - max_history;
history.drain(start..start + to_remove);
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct InteractiveSessionState {
pub(crate) version: u32,
pub(crate) history: Vec<ChatMessage>,
}
impl InteractiveSessionState {
fn from_history(history: &[ChatMessage]) -> Self {
Self {
version: 1,
history: history.to_vec(),
}
}
}
pub(crate) fn load_interactive_session_history(
path: &Path,
system_prompt: &str,
) -> Result<Vec<ChatMessage>> {
if !path.exists() {
return Ok(vec![ChatMessage::system(system_prompt)]);
}
let raw = std::fs::read_to_string(path)?;
let mut state: InteractiveSessionState = serde_json::from_str(&raw)?;
if state.history.is_empty() {
state.history.push(ChatMessage::system(system_prompt));
} else if state.history.first().map(|msg| msg.role.as_str()) != Some("system") {
state.history.insert(0, ChatMessage::system(system_prompt));
}
Ok(state.history)
}
pub(crate) fn save_interactive_session_history(path: &Path, history: &[ChatMessage]) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let payload = serde_json::to_string_pretty(&InteractiveSessionState::from_history(history))?;
std::fs::write(path, payload)?;
Ok(())
}