use theway_llm_provider::{Message as PiMessage, ToolResultMessage, UserContentBlock};
use crate::types::AgentMessage;
pub const TOOL_RESULT_VIRTUALIZATION_MAX_CHARS: usize = 20_000;
pub const TOOL_RESULT_VIRTUALIZATION_MAX_CHARS_ENV: &str = "THEWAY_TOOL_RESULT_MAX_CHARS";
pub const TOOL_RESULT_TAIL_PREVIEW_LINES: usize = 5;
pub const TOOL_RESULT_TAIL_PREVIEW_LINE_CHARS: usize = 200;
pub const TOOL_RESULT_FRONT_PREVIEW_CHARS: usize = 200;
fn effective_max_chars() -> usize {
std::env::var(TOOL_RESULT_VIRTUALIZATION_MAX_CHARS_ENV)
.ok()
.and_then(|value| value.trim().parse::<usize>().ok())
.filter(|&value| value > 0)
.unwrap_or(TOOL_RESULT_VIRTUALIZATION_MAX_CHARS)
}
pub fn virtualize_tool_results(messages: Vec<AgentMessage>) -> Vec<AgentMessage> {
virtualize_tool_results_with_max_chars(messages, effective_max_chars())
}
pub(crate) fn virtualize_tool_results_with_max_chars(
messages: Vec<AgentMessage>,
max_chars: usize,
) -> Vec<AgentMessage> {
messages
.into_iter()
.map(|message| match message {
AgentMessage::Llm(PiMessage::ToolResult(result)) => AgentMessage::Llm(
PiMessage::ToolResult(virtualize_tool_result(result, max_chars)),
),
other => other,
})
.collect()
}
fn virtualize_tool_result(mut result: ToolResultMessage, max_chars: usize) -> ToolResultMessage {
let full_text = tool_result_text(&result);
if full_text.chars().count() <= max_chars {
return result;
}
let chars = full_text.chars().count();
let lines = count_lines(&full_text);
let front = front_preview(&full_text, TOOL_RESULT_FRONT_PREVIEW_CHARS);
let tail = tail_preview(
&full_text,
TOOL_RESULT_TAIL_PREVIEW_LINES,
TOOL_RESULT_TAIL_PREVIEW_LINE_CHARS,
);
let placeholder = format!(
"[tool_result {} {}: {} / {}, exit {}; front: {}; tail: {}]",
result.tool_name,
result.tool_call_id,
chars,
lines,
exit_code(&result),
front,
tail
);
result.content = vec![UserContentBlock::text(placeholder)];
result
}
fn tool_result_text(result: &ToolResultMessage) -> String {
if let Some(details) = &result.details {
if let Some(full) = details.get("full_text").and_then(|v| v.as_str()) {
return full.to_string();
}
}
result
.content
.iter()
.filter_map(|block| match block {
UserContentBlock::Text(text) => Some(text.text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
}
fn front_preview(text: &str, max_chars: usize) -> String {
let chars: Vec<char> = text.chars().collect();
if chars.len() <= max_chars {
text.to_string()
} else {
let head: String = chars.into_iter().take(max_chars).collect();
format!("{head}…")
}
}
fn count_lines(text: &str) -> usize {
text.split_inclusive('\n').count()
}
fn tail_preview(text: &str, max_lines: usize, max_chars: usize) -> String {
let lines: Vec<&str> = text.split_inclusive('\n').collect();
let tail: Vec<&str> = lines.iter().rev().take(max_lines).rev().copied().collect();
tail.into_iter()
.map(|line| truncate_preview_line(line, max_chars))
.collect()
}
fn truncate_preview_line(line: &str, max_chars: usize) -> String {
let chars: Vec<char> = line.chars().collect();
if chars.len() <= max_chars {
line.to_string()
} else {
let head: String = chars.into_iter().take(max_chars).collect();
format!("{head}…")
}
}
fn exit_code(result: &ToolResultMessage) -> String {
if let Some(details) = &result.details {
for key in ["exitCode", "exit_code"] {
if let Some(value) = details.get(key) {
if let Some(code) = value.as_i64() {
return code.to_string();
}
if let Some(code) = value.as_u64() {
return code.to_string();
}
}
}
}
if result.is_error {
"1".into()
} else {
"0".into()
}
}