@@ -2,12 +2,11 @@ use codex_protocol::models::FunctionCallOutputContentItem;
22use codex_utils_string:: take_bytes_at_char_boundary;
33use codex_utils_string:: take_last_bytes_at_char_boundary;
44
5+ use crate :: util:: error_or_panic;
6+
57// Model-formatting limits: clients get full streams; only content sent to the model is truncated.
6- pub ( crate ) const MODEL_FORMAT_MAX_BYTES : usize = 10 * 1024 ; // 10 KiB
7- pub ( crate ) const MODEL_FORMAT_MAX_LINES : usize = 256 ; // lines
8- pub ( crate ) const MODEL_FORMAT_HEAD_LINES : usize = MODEL_FORMAT_MAX_LINES / 2 ;
9- pub ( crate ) const MODEL_FORMAT_TAIL_LINES : usize = MODEL_FORMAT_MAX_LINES - MODEL_FORMAT_HEAD_LINES ; // 128
10- pub ( crate ) const MODEL_FORMAT_HEAD_BYTES : usize = MODEL_FORMAT_MAX_BYTES / 2 ;
8+ pub const MODEL_FORMAT_MAX_BYTES : usize = 10 * 1024 ; // 10 KiB
9+ pub const MODEL_FORMAT_MAX_LINES : usize = 256 ; // lines
1110
1211pub ( crate ) fn globally_truncate_function_output_items (
1312 items : & [ FunctionCallOutputContentItem ] ,
@@ -56,21 +55,34 @@ pub(crate) fn globally_truncate_function_output_items(
5655 out
5756}
5857
59- pub ( crate ) fn format_output_for_model_body ( content : & str ) -> String {
58+ pub ( crate ) fn format_output_for_model_body (
59+ content : & str ,
60+ limit_bytes : usize ,
61+ limit_lines : usize ,
62+ ) -> String {
6063 // Head+tail truncation for the model: show the beginning and end with an elision.
6164 // Clients still receive full streams; only this formatted summary is capped.
6265 let total_lines = content. lines ( ) . count ( ) ;
63- if content. len ( ) <= MODEL_FORMAT_MAX_BYTES && total_lines <= MODEL_FORMAT_MAX_LINES {
66+ if content. len ( ) <= limit_bytes && total_lines <= limit_lines {
6467 return content. to_string ( ) ;
6568 }
66- let output = truncate_formatted_exec_output ( content, total_lines) ;
69+ let output = truncate_formatted_exec_output ( content, total_lines, limit_bytes , limit_lines ) ;
6770 format ! ( "Total output lines: {total_lines}\n \n {output}" )
6871}
6972
70- fn truncate_formatted_exec_output ( content : & str , total_lines : usize ) -> String {
73+ fn truncate_formatted_exec_output (
74+ content : & str ,
75+ total_lines : usize ,
76+ limit_bytes : usize ,
77+ limit_lines : usize ,
78+ ) -> String {
79+ debug_panic_on_double_truncation ( content) ;
80+ let head_lines: usize = limit_lines / 2 ;
81+ let tail_lines: usize = limit_lines - head_lines; // 128
82+ let head_bytes: usize = limit_bytes / 2 ;
7183 let segments: Vec < & str > = content. split_inclusive ( '\n' ) . collect ( ) ;
72- let head_take = MODEL_FORMAT_HEAD_LINES . min ( segments. len ( ) ) ;
73- let tail_take = MODEL_FORMAT_TAIL_LINES . min ( segments. len ( ) . saturating_sub ( head_take) ) ;
84+ let head_take = head_lines . min ( segments. len ( ) ) ;
85+ let tail_take = tail_lines . min ( segments. len ( ) . saturating_sub ( head_take) ) ;
7486 let omitted = segments. len ( ) . saturating_sub ( head_take + tail_take) ;
7587
7688 let head_slice_end: usize = segments
@@ -91,32 +103,32 @@ fn truncate_formatted_exec_output(content: &str, total_lines: usize) -> String {
91103 } ;
92104 let head_slice = & content[ ..head_slice_end] ;
93105 let tail_slice = & content[ tail_slice_start..] ;
94- let truncated_by_bytes = content. len ( ) > MODEL_FORMAT_MAX_BYTES ;
106+ let truncated_by_bytes = content. len ( ) > limit_bytes ;
95107 // this is a bit wrong. We are counting metadata lines and not just shell output lines.
96108 let marker = if omitted > 0 {
97109 Some ( format ! (
98110 "\n [... omitted {omitted} of {total_lines} lines ...]\n \n "
99111 ) )
100112 } else if truncated_by_bytes {
101113 Some ( format ! (
102- "\n [... output truncated to fit {MODEL_FORMAT_MAX_BYTES } bytes ...]\n \n "
114+ "\n [... output truncated to fit {limit_bytes } bytes ...]\n \n "
103115 ) )
104116 } else {
105117 None
106118 } ;
107119
108120 let marker_len = marker. as_ref ( ) . map_or ( 0 , String :: len) ;
109- let base_head_budget = MODEL_FORMAT_HEAD_BYTES . min ( MODEL_FORMAT_MAX_BYTES ) ;
110- let head_budget = base_head_budget. min ( MODEL_FORMAT_MAX_BYTES . saturating_sub ( marker_len) ) ;
121+ let base_head_budget = head_bytes . min ( limit_bytes ) ;
122+ let head_budget = base_head_budget. min ( limit_bytes . saturating_sub ( marker_len) ) ;
111123 let head_part = take_bytes_at_char_boundary ( head_slice, head_budget) ;
112- let mut result = String :: with_capacity ( MODEL_FORMAT_MAX_BYTES . min ( content. len ( ) ) ) ;
124+ let mut result = String :: with_capacity ( limit_bytes . min ( content. len ( ) ) ) ;
113125
114126 result. push_str ( head_part) ;
115127 if let Some ( marker_text) = marker. as_ref ( ) {
116128 result. push_str ( marker_text) ;
117129 }
118130
119- let remaining = MODEL_FORMAT_MAX_BYTES . saturating_sub ( result. len ( ) ) ;
131+ let remaining = limit_bytes . saturating_sub ( result. len ( ) ) ;
120132 if remaining == 0 {
121133 return result;
122134 }
@@ -126,3 +138,11 @@ fn truncate_formatted_exec_output(content: &str, total_lines: usize) -> String {
126138
127139 result
128140}
141+
142+ fn debug_panic_on_double_truncation ( content : & str ) {
143+ if content. contains ( "Total output lines:" ) && content. contains ( "omitted" ) {
144+ error_or_panic ( format ! (
145+ "FunctionCallOutput content was already truncated before ContextManager::record_items; this would cause double truncation {content}"
146+ ) ) ;
147+ }
148+ }
0 commit comments