Skip to content

Commit 177d51f

Browse files
committed
fix(app-server): truncate tool previews at utf8 boundaries
1 parent 7954d02 commit 177d51f

4 files changed

Lines changed: 46 additions & 4 deletions

File tree

src/cortex-app-server/src/tools/filesystem.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
44

55
use serde_json::{Value, json};
66

7+
use super::preview::truncate_at_char_boundary;
78
use super::types::ToolResult;
89

910
/// Read file contents.
@@ -119,7 +120,7 @@ pub async fn write_file(cwd: &Path, args: Value) -> ToolResult {
119120
"filename": std::path::Path::new(path).file_name().and_then(|n| n.to_str()).unwrap_or(""),
120121
"extension": extension,
121122
"size": content.len(),
122-
"content_preview": if content.len() > 500 { &content[..500] } else { content }
123+
"content_preview": truncate_at_char_boundary(content, 500)
123124
})),
124125
},
125126
Err(e) => ToolResult::error(format!("Failed to write file: {e}")),

src/cortex-app-server/src/tools/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ mod definitions;
1515
mod executor;
1616
mod filesystem;
1717
mod planning;
18+
mod preview;
1819
mod search;
1920
mod security;
2021
mod shell;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//! Helpers for building tool output previews.
2+
3+
/// Truncate a string to at most `max_bytes` bytes without splitting a UTF-8 character.
4+
pub(super) fn truncate_at_char_boundary(input: &str, max_bytes: usize) -> &str {
5+
if input.len() <= max_bytes {
6+
return input;
7+
}
8+
9+
let mut end = max_bytes;
10+
while end > 0 && !input.is_char_boundary(end) {
11+
end -= 1;
12+
}
13+
14+
&input[..end]
15+
}
16+
17+
#[cfg(test)]
18+
mod tests {
19+
use super::truncate_at_char_boundary;
20+
21+
#[test]
22+
fn keeps_ascii_at_requested_byte_limit() {
23+
assert_eq!(truncate_at_char_boundary("abcdef", 3), "abc");
24+
}
25+
26+
#[test]
27+
fn backs_up_to_utf8_boundary() {
28+
let input = format!("{}{}", "A".repeat(499), "\u{4E2D}");
29+
30+
assert_eq!(input.len(), 502);
31+
assert_eq!(truncate_at_char_boundary(&input, 500), "A".repeat(499));
32+
}
33+
34+
#[test]
35+
fn returns_full_input_when_under_limit() {
36+
assert_eq!(truncate_at_char_boundary("hello", 500), "hello");
37+
}
38+
}

src/cortex-app-server/src/tools/web.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use serde_json::Value;
44
use tokio::process::Command;
55

6+
use super::preview::truncate_at_char_boundary;
67
use super::types::ToolResult;
78

89
/// Fetch content from a URL.
@@ -79,9 +80,10 @@ pub async fn fetch_url(args: Value) -> ToolResult {
7980

8081
// Truncate for display if too long
8182
let truncated = if content.len() > 100_000 {
83+
let preview = truncate_at_char_boundary(&content, 100_000);
8284
format!(
83-
"{}...\n[Truncated at 100000 chars, full size: {} chars]",
84-
&content[..100_000],
85+
"{}...\n[Truncated at 100000 bytes, full size: {} bytes]",
86+
preview,
8587
content.len()
8688
)
8789
} else {
@@ -124,7 +126,7 @@ pub async fn web_search(args: Value) -> ToolResult {
124126
let html = String::from_utf8_lossy(&output.stdout);
125127
// Simple extraction of text
126128
let truncated = if html.len() > 10_000 {
127-
&html[..10_000]
129+
truncate_at_char_boundary(&html, 10_000)
128130
} else {
129131
&html
130132
};

0 commit comments

Comments
 (0)