[codex] Warn clearly when code mode output is truncated (#28467)

## Summary

- make `formatted_truncate_text` prepend `Warning: truncated output
(original token count: N)` above the existing `Total output lines`
header
- update direct formatter, unified-exec, user-shell, and code-mode
expectations
- add core unit coverage that runs in Bazel without requiring the
skipped V8-backed code-mode integration suite

## Validation

- `cargo test -p codex-utils-output-truncation -- --nocapture` (17
passed)
- `cargo test -p codex-core --lib
truncated_text_output_starts_with_warning -- --nocapture`
- `cargo test -p codex-core --test all
clamps_model_requested_max_output_tokens_to_policy -- --nocapture` (2
passed)
- `cargo test -p codex-core --test all
unified_exec_formats_large_output_summary -- --nocapture`
- `cargo test -p codex-core --test all
user_shell_command_output_is_truncated_in_history -- --nocapture`
- Bazel CI exercises the shared formatter and downstream integration
expectations
This commit is contained in:
Ahmed Ibrahim
2026-06-16 10:37:06 -07:00
committed by GitHub
Unverified
parent 3ded846488
commit 952656356a
6 changed files with 47 additions and 20 deletions
+21
View File
@@ -321,8 +321,10 @@ fn build_freeform_tool_payload(
#[cfg(test)]
mod tests {
use super::build_nested_tool_payload;
use super::truncate_code_mode_result;
use crate::tools::context::ToolPayload;
use codex_code_mode::CodeModeToolKind;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_tools::ToolName;
use serde_json::json;
@@ -359,4 +361,23 @@ mod tests {
other => panic!("expected freeform payload, got {other:?}"),
}
}
#[test]
fn truncated_text_output_starts_with_warning() {
let items = vec![FunctionCallOutputContentItem::InputText {
text: "0123456789012345678901234567890123456789".to_string(),
}];
assert_eq!(
truncate_code_mode_result(items, Some(5)),
vec![FunctionCallOutputContentItem::InputText {
text: concat!(
"Warning: truncated output (original token count: 10)\n",
"Total output lines: 1\n\n",
"0123456789…5 tokens truncated…0123456789"
)
.to_string(),
}]
);
}
}
+3 -2
View File
@@ -1035,7 +1035,7 @@ text(result.output);
/*index*/ 1
),
format!(
"Total output lines: 1\n\n{}…2500 tokens truncated…{}",
"Warning: truncated output (original token count: 22500)\nTotal output lines: 1\n\n{}…2500 tokens truncated…{}",
"A".repeat(40_000),
"A".repeat(40_000)
)
@@ -1176,7 +1176,7 @@ text(result.output);
&custom_tool_output_items(&second_mock.single_request(), "call-1"),
/*index*/ 1
),
"Total output lines: 1\n\n0123456789…5 tokens truncated…0123456789"
"Warning: truncated output (original token count: 10)\nTotal output lines: 1\n\n0123456789…5 tokens truncated…0123456789"
);
Ok(())
@@ -2389,6 +2389,7 @@ text("token one token two token three token four token five token six token seve
);
let expected_pattern = r#"(?sx)
\A
Warning:\ truncated\ output\ \(original\ token\ count:\ \d+\)\n
Total\ output\ lines:\ 1\n
\n
.*…\d+\ tokens\ truncated….*
+4 -4
View File
@@ -72,7 +72,7 @@ fn parse_unified_exec_output(raw: &str) -> Result<ParsedUnifiedExecOutput> {
static OUTPUT_REGEX: OnceLock<Regex> = OnceLock::new();
let regex = OUTPUT_REGEX.get_or_init(|| {
Regex::new(concat!(
r#"(?s)^(?:Total output lines: \d+\n\n)?"#,
r#"(?s)^(?:Warning: truncated output \(original token count: \d+\)\n)?(?:Total output lines: \d+\n\n)?"#,
r#"(?:Chunk ID: (?P<chunk_id>[^\n]+)\n)?"#,
r#"Wall time: (?P<wall_time>-?\d+(?:\.\d+)?) seconds\n"#,
r#"(?:Process exited with code (?P<exit_code>-?\d+)\n)?"#,
@@ -1554,7 +1554,7 @@ async fn exec_command_clamps_model_requested_max_output_tokens_to_policy() -> Re
assert_eq!(output.original_token_count, Some(8_991));
let output_text = output.output.replace("\r\n", "\n");
assert_regex_match(
r"^Total output lines: 999\n\nEXEC-LINE-0001 x{20}\nEXEC-LINE-0002 x{20}\nEXEC-LINE-0003 x{13}…8941 tokens truncated…E-0997 x{20}\nEXEC-LINE-0998 x{20}\nEXEC-LINE-0999 x{20}\n$",
r"^Warning: truncated output \(original token count: 8991\)\nTotal output lines: 999\n\nEXEC-LINE-0001 x{20}\nEXEC-LINE-0002 x{20}\nEXEC-LINE-0003 x{13}…8941 tokens truncated…E-0997 x{20}\nEXEC-LINE-0998 x{20}\nEXEC-LINE-0999 x{20}\n$",
&output_text,
);
@@ -1645,7 +1645,7 @@ async fn write_stdin_clamps_model_requested_max_output_tokens_to_policy() -> Res
assert_eq!(stdin_output.original_token_count, Some(9_492));
let stdin_output_text = stdin_output.output.replace("\r\n", "\n");
assert_regex_match(
r"^Total output lines: 1000\n\ngo\nSTDIN-LINE-0001 y{20}\nSTDIN-LINE-0002 y{20}\nSTDIN-LINE-0003 yyyy…9442 tokens truncated…7 y{20}\nSTDIN-LINE-0998 y{20}\nSTDIN-LINE-0999 y{20}\n$",
r"^Warning: truncated output \(original token count: 9492\)\nTotal output lines: 1000\n\ngo\nSTDIN-LINE-0001 y{20}\nSTDIN-LINE-0002 y{20}\nSTDIN-LINE-0003 yyyy…9442 tokens truncated…7 y{20}\nSTDIN-LINE-0998 y{20}\nSTDIN-LINE-0999 y{20}\n$",
&stdin_output_text,
);
@@ -2960,7 +2960,7 @@ PY
let large_output = outputs.get(call_id).expect("missing large output summary");
let output_text = large_output.output.replace("\r\n", "\n");
let truncated_pattern = r"(?s)^Total output lines: \d+\n\n(token token \n){5,}.*…\d+ tokens truncated….*(token token \n){5,}$";
let truncated_pattern = r"(?s)^Warning: truncated output \(original token count: \d+\)\nTotal output lines: \d+\n\n(token token \n){5,}.*…\d+ tokens truncated….*(token token \n){5,}$";
assert_regex_match(truncated_pattern, &output_text);
let original_tokens = large_output
+3 -2
View File
@@ -476,8 +476,9 @@ async fn user_shell_command_output_is_truncated_in_history() -> anyhow::Result<(
let head = (1..=69).map(|i| format!("{i}\n")).collect::<String>();
let tail = (352..=400).map(|i| format!("{i}\n")).collect::<String>();
let truncated_body =
format!("Total output lines: 400\n\n{head}70…273 tokens truncated…351\n{tail}");
let truncated_body = format!(
"Warning: truncated output (original token count: 373)\nTotal output lines: 400\n\n{head}70…273 tokens truncated…351\n{tail}"
);
let escaped_command = escape(&command);
let escaped_truncated_body = escape(&truncated_body);
let expected_pattern = format!(
+6 -2
View File
@@ -14,9 +14,12 @@ pub fn formatted_truncate_text(content: &str, policy: TruncationPolicy) -> Strin
return content.to_string();
}
let original_token_count = approx_token_count(content);
let total_lines = content.lines().count();
let result = truncate_text(content, policy);
format!("Total output lines: {total_lines}\n\n{result}")
format!(
"Warning: truncated output (original token count: {original_token_count})\nTotal output lines: {total_lines}\n\n{result}"
)
}
pub fn truncate_text(content: &str, policy: TruncationPolicy) -> String {
@@ -55,6 +58,7 @@ pub fn formatted_truncate_text_content_items_with_policy(
return (items.to_vec(), None);
}
let original_token_count = approx_token_count(&combined);
let mut out = vec![FunctionCallOutputContentItem::InputText {
text: formatted_truncate_text(&combined, policy),
}];
@@ -73,7 +77,7 @@ pub fn formatted_truncate_text_content_items_with_policy(
FunctionCallOutputContentItem::InputText { .. } => None,
}));
(out, Some(approx_token_count(&combined)))
(out, Some(original_token_count))
}
pub fn truncate_function_output_items_with_policy(
@@ -14,7 +14,7 @@ fn truncate_bytes_less_than_placeholder_returns_placeholder() {
let content = "example output";
assert_eq!(
"Total output lines: 1\n\n…13 chars truncated…t",
"Warning: truncated output (original token count: 4)\nTotal output lines: 1\n\n…13 chars truncated…t",
formatted_truncate_text(content, TruncationPolicy::Bytes(1)),
);
}
@@ -24,7 +24,7 @@ fn truncate_tokens_less_than_placeholder_returns_placeholder() {
let content = "example output";
assert_eq!(
"Total output lines: 1\n\nex…3 tokens truncated…ut",
"Warning: truncated output (original token count: 4)\nTotal output lines: 1\n\nex…3 tokens truncated…ut",
formatted_truncate_text(content, TruncationPolicy::Tokens(1)),
);
}
@@ -54,7 +54,7 @@ fn truncate_tokens_over_limit_returns_truncated() {
let content = "this is an example of a long output that should be truncated";
assert_eq!(
"Total output lines: 1\n\nthis is an…10 tokens truncated… truncated",
"Warning: truncated output (original token count: 15)\nTotal output lines: 1\n\nthis is an…10 tokens truncated… truncated",
formatted_truncate_text(content, TruncationPolicy::Tokens(5)),
);
}
@@ -64,7 +64,7 @@ fn truncate_bytes_over_limit_returns_truncated() {
let content = "this is an example of a long output that should be truncated";
assert_eq!(
"Total output lines: 1\n\nthis is an exam…30 chars truncated…ld be truncated",
"Warning: truncated output (original token count: 15)\nTotal output lines: 1\n\nthis is an exam…30 chars truncated…ld be truncated",
formatted_truncate_text(content, TruncationPolicy::Bytes(30)),
);
}
@@ -75,7 +75,7 @@ fn truncate_bytes_reports_original_line_count_when_truncated() {
"this is an example of a long output that should be truncated\nalso some other line";
assert_eq!(
"Total output lines: 2\n\nthis is an exam…51 chars truncated…some other line",
"Warning: truncated output (original token count: 21)\nTotal output lines: 2\n\nthis is an exam…51 chars truncated…some other line",
formatted_truncate_text(content, TruncationPolicy::Bytes(30)),
);
}
@@ -86,7 +86,7 @@ fn truncate_tokens_reports_original_line_count_when_truncated() {
"this is an example of a long output that should be truncated\nalso some other line";
assert_eq!(
"Total output lines: 2\n\nthis is an example o…11 tokens truncated…also some other line",
"Warning: truncated output (original token count: 21)\nTotal output lines: 2\n\nthis is an example o…11 tokens truncated…also some other line",
formatted_truncate_text(content, TruncationPolicy::Tokens(10)),
);
}
@@ -201,7 +201,7 @@ fn formatted_truncate_text_content_items_with_policy_preserves_empty_leading_tex
assert_eq!(
output,
vec![FunctionCallOutputContentItem::InputText {
text: "Total output lines: 1\n\n…3 chars truncated…".to_string(),
text: "Warning: truncated output (original token count: 1)\nTotal output lines: 1\n\n…3 chars truncated…".to_string(),
}]
);
assert_eq!(original_token_count, Some(1));
@@ -236,7 +236,7 @@ fn formatted_truncate_text_content_items_with_policy_merges_text_and_appends_ima
output,
vec![
FunctionCallOutputContentItem::InputText {
text: "Total output lines: 3\n\nabcd…6 chars truncated…ijkl".to_string(),
text: "Warning: truncated output (original token count: 4)\nTotal output lines: 3\n\nabcd…6 chars truncated…ijkl".to_string(),
},
FunctionCallOutputContentItem::InputImage {
image_url: "img:one".to_string(),
@@ -269,7 +269,7 @@ fn formatted_truncate_text_content_items_with_policy_preserves_encrypted_content
output,
vec![
FunctionCallOutputContentItem::InputText {
text: "Total output lines: 1\n\na…6 chars truncated…h".to_string(),
text: "Warning: truncated output (original token count: 2)\nTotal output lines: 1\n\na…6 chars truncated…h".to_string(),
},
FunctionCallOutputContentItem::EncryptedContent {
encrypted_content: "enc_opaque".to_string(),
@@ -322,7 +322,7 @@ fn formatted_truncate_text_content_items_with_policy_merges_all_text_for_token_b
assert_eq!(
output,
vec![FunctionCallOutputContentItem::InputText {
text: "Total output lines: 2\n\nabcd…3 tokens truncated…mnop".to_string(),
text: "Warning: truncated output (original token count: 5)\nTotal output lines: 2\n\nabcd…3 tokens truncated…mnop".to_string(),
}]
);
assert_eq!(original_token_count, Some(5));