fix(function): floor tool-output truncation cut to a UTF-8 char boundary
When max_chars landed inside a multi-byte UTF-8 character of the serialized output, s.get(..max_chars) returned None and the code fell back to the FULL untruncated string while still prepending the truncation marker — the "truncated" output actually grew. The cut is now floored to the previous char boundary so the prefix is always a valid, genuinely truncated slice.
This commit is contained in:
+44
-9
@@ -405,7 +405,11 @@ impl ToolResult {
|
|||||||
pub fn truncate_if_needed(mut self, max_chars: usize) -> Self {
|
pub fn truncate_if_needed(mut self, max_chars: usize) -> Self {
|
||||||
let s = self.output.to_string();
|
let s = self.output.to_string();
|
||||||
if s.len() > max_chars {
|
if s.len() > max_chars {
|
||||||
let prefix = s.get(..max_chars).unwrap_or(s.as_str());
|
let mut cut = max_chars;
|
||||||
|
while !s.is_char_boundary(cut) {
|
||||||
|
cut -= 1;
|
||||||
|
}
|
||||||
|
let prefix = &s[..cut];
|
||||||
self.output = json!(format!(
|
self.output = json!(format!(
|
||||||
"[truncated: tool output exceeded {max_chars} chars]\n{prefix}"
|
"[truncated: tool output exceeded {max_chars} chars]\n{prefix}"
|
||||||
));
|
));
|
||||||
@@ -4179,23 +4183,54 @@ mod tests {
|
|||||||
assert!(!results[0].output.to_string().contains("[truncated"));
|
assert!(!results[0].output.to_string().contains("[truncated"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pins current behavior: when the char cap lands inside a multi-byte
|
/// When the char cap lands inside a multi-byte UTF-8 character of the
|
||||||
/// UTF-8 character of the serialized output, no prefix can be taken, so
|
/// serialized output, the cut is floored to the previous char boundary
|
||||||
/// the truncation marker is prepended to the FULL original output and the
|
/// so the output actually shrinks.
|
||||||
/// "truncated" result is longer than the input.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn truncate_if_needed_utf8_boundary_returns_full_output_with_marker() {
|
fn truncate_if_needed_floors_cut_to_char_boundary() {
|
||||||
let serialized = json!("aé").to_string();
|
let serialized = json!("aé").to_string();
|
||||||
let result = ToolResult::new(call("t", Some("id-1")), json!("aé"));
|
let result = ToolResult::new(call("t", Some("id-1")), json!("aé"));
|
||||||
|
|
||||||
let truncated = result.truncate_if_needed(3);
|
let truncated = result.truncate_if_needed(3);
|
||||||
|
|
||||||
let out = truncated.output.as_str().unwrap();
|
let out = truncated.output.as_str().unwrap();
|
||||||
|
assert_eq!(out, "[truncated: tool output exceeded 3 chars]\n\"a");
|
||||||
|
assert!(out.len() < "[truncated: tool output exceeded 3 chars]\n".len() + serialized.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncate_if_needed_cap_on_char_boundary_truncates_normally() {
|
||||||
|
let result = ToolResult::new(call("t", Some("id-1")), json!("aé"));
|
||||||
|
|
||||||
|
let truncated = result.truncate_if_needed(2);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
out,
|
truncated.output.as_str().unwrap(),
|
||||||
format!("[truncated: tool output exceeded 3 chars]\n{serialized}")
|
"[truncated: tool output exceeded 2 chars]\n\"a"
|
||||||
);
|
);
|
||||||
assert!(out.len() > serialized.len());
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncate_if_needed_cap_zero_yields_marker_and_empty_prefix() {
|
||||||
|
let result = ToolResult::new(call("t", Some("id-1")), json!("aé"));
|
||||||
|
|
||||||
|
let truncated = result.truncate_if_needed(0);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
truncated.output.as_str().unwrap(),
|
||||||
|
"[truncated: tool output exceeded 0 chars]\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncate_if_needed_cap_at_or_above_length_leaves_output_unchanged() {
|
||||||
|
for cap in [5, 100] {
|
||||||
|
let result = ToolResult::new(call("t", Some("id-1")), json!("aé"));
|
||||||
|
|
||||||
|
let truncated = result.truncate_if_needed(cap);
|
||||||
|
|
||||||
|
assert_eq!(truncated.output, json!("aé"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user