@@ -549,7 +549,13 @@ impl RAG {
549549 return context. to_string ( ) ;
550550 }
551551
552- format ! ( "{}..." , & context[ ..char_limit] )
552+ // `char_limit` is a byte count; walk back to the nearest char boundary
553+ // so we never slice through a multi-byte UTF-8 character (would panic).
554+ let mut end = char_limit. min ( context. len ( ) ) ;
555+ while end > 0 && !context. is_char_boundary ( end) {
556+ end -= 1 ;
557+ }
558+ format ! ( "{}..." , & context[ ..end] )
553559 }
554560}
555561
@@ -616,7 +622,13 @@ pub fn truncate_context(context: &str, max_tokens: usize) -> String {
616622 return context. to_string ( ) ;
617623 }
618624
619- format ! ( "{}..." , & context[ ..char_limit] )
625+ // `char_limit` is a byte count; walk back to the nearest char boundary so
626+ // we never slice through a multi-byte UTF-8 character (which would panic).
627+ let mut end = char_limit. min ( context. len ( ) ) ;
628+ while end > 0 && !context. is_char_boundary ( end) {
629+ end -= 1 ;
630+ }
631+ format ! ( "{}..." , & context[ ..end] )
620632}
621633
622634/// Deduplicate chunks by content similarity.
@@ -760,6 +772,16 @@ mod tests {
760772 assert ! ( truncated. ends_with( "..." ) ) ;
761773 }
762774
775+ #[ test]
776+ fn test_truncate_context_multibyte_no_panic ( ) {
777+ // A byte-based char_limit can land inside a multi-byte character;
778+ // truncation must not panic and must cut on a char boundary.
779+ let text = "日本語テキストです" . repeat ( 50 ) ; // multi-byte, well over the limit
780+ let truncated = truncate_context ( & text, 1 ) ;
781+ assert ! ( truncated. ends_with( "..." ) ) ;
782+ assert ! ( truncated. len( ) < text. len( ) ) ;
783+ }
784+
763785 #[ test]
764786 fn test_deduplicate_chunks ( ) {
765787 let chunks = vec ! [
0 commit comments