diff --git a/codex-rs/core/src/tools/code_mode/mod.rs b/codex-rs/core/src/tools/code_mode/mod.rs index ade23cf3b770..92bb5a8ad634 100644 --- a/codex-rs/core/src/tools/code_mode/mod.rs +++ b/codex-rs/core/src/tools/code_mode/mod.rs @@ -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(), + }] + ); + } } diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index e436b6fa9b3b..25fc3ccf2b3c 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -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….* diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index 9f8d931da230..8ef694a408ed 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -72,7 +72,7 @@ fn parse_unified_exec_output(raw: &str) -> Result { static OUTPUT_REGEX: OnceLock = 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[^\n]+)\n)?"#, r#"Wall time: (?P-?\d+(?:\.\d+)?) seconds\n"#, r#"(?:Process exited with code (?P-?\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 diff --git a/codex-rs/core/tests/suite/user_shell_cmd.rs b/codex-rs/core/tests/suite/user_shell_cmd.rs index 4a9605497b15..174aba27e492 100644 --- a/codex-rs/core/tests/suite/user_shell_cmd.rs +++ b/codex-rs/core/tests/suite/user_shell_cmd.rs @@ -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::(); let tail = (352..=400).map(|i| format!("{i}\n")).collect::(); - 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!( diff --git a/codex-rs/utils/output-truncation/src/lib.rs b/codex-rs/utils/output-truncation/src/lib.rs index 52cd741b7d09..4fcff7dce4f4 100644 --- a/codex-rs/utils/output-truncation/src/lib.rs +++ b/codex-rs/utils/output-truncation/src/lib.rs @@ -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( diff --git a/codex-rs/utils/output-truncation/src/truncate_tests.rs b/codex-rs/utils/output-truncation/src/truncate_tests.rs index baf26a058ece..e8907cf8f859 100644 --- a/codex-rs/utils/output-truncation/src/truncate_tests.rs +++ b/codex-rs/utils/output-truncation/src/truncate_tests.rs @@ -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));