diff --git a/src/agent/routine_engine.rs b/src/agent/routine_engine.rs index 994c857e..e15400ce 100644 --- a/src/agent/routine_engine.rs +++ b/src/agent/routine_engine.rs @@ -1988,19 +1988,37 @@ fn sanitize_summary(s: &str) -> String { } } -/// Remove HTML/XML tags from a string. +/// Remove actual HTML tags from a string while preserving non-HTML angle brackets. +/// +/// Only strips patterns that look like real HTML/XML tags (e.g. `
`, `

`, +/// ``), not generic angle-bracket content like `Vec`, +/// `cat < input.txt`, or comparison operators. fn strip_html_tags(s: &str) -> String { - let mut result = String::with_capacity(s.len()); - let mut in_tag = false; - for c in s.chars() { - match c { - '<' => in_tag = true, - '>' if in_tag => in_tag = false, - _ if !in_tag => result.push(c), - _ => {} - } - } - result + use std::sync::LazyLock; + + // Matches opening tags like
, , , + // closing tags like

, , and self-closing tags like
. + // Does NOT match things like Vec, x<10, or < input.txt because those + // don't have a letter immediately after '<' followed by valid tag structure, + // or they aren't among recognized HTML tag names. + static HTML_TAG_RE: LazyLock = LazyLock::new(|| { + // Match or where tagname starts with a letter. + // We restrict to known HTML tag names to avoid false positives on generic + // identifiers like Vec. + let tags = "a|abbr|address|area|article|aside|audio|b|base|bdi|bdo|blockquote|\ + body|br|button|canvas|caption|cite|code|col|colgroup|data|datalist|dd|del|\ + details|dfn|dialog|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|\ + form|h[1-6]|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|label|\ + legend|li|link|main|map|mark|meta|meter|nav|noscript|object|ol|optgroup|\ + option|output|p|param|picture|pre|progress|q|rp|rt|ruby|s|samp|script|\ + section|select|slot|small|source|span|strong|style|sub|summary|sup|table|\ + tbody|td|template|textarea|tfoot|th|thead|time|title|tr|track|u|ul|var|\ + video|wbr"; + Regex::new(&format!(r"(?i)]*)?>", tags)) + .expect("HTML_TAG_RE is a valid static regex") + }); + + HTML_TAG_RE.replace_all(s, "").into_owned() } #[cfg(test)] @@ -2574,6 +2592,33 @@ mod tests { assert_eq!(sanitize_summary(""), ""); } + #[test] + fn test_sanitize_summary_preserves_non_html_angle_brackets() { + use super::sanitize_summary; + + // Rust/Java generics must pass through unchanged + assert_eq!( + sanitize_summary("expected Vec"), + "expected Vec" + ); + assert_eq!( + sanitize_summary("HashMap>"), + "HashMap>" + ); + + // Shell redirects must pass through unchanged + assert_eq!(sanitize_summary("cat < input.txt"), "cat < input.txt"); + + // Comparison operators must pass through unchanged + assert_eq!(sanitize_summary("x < 10 && y > 20"), "x < 10 && y > 20"); + + // Mixed: real HTML stripped but generics preserved + assert_eq!( + sanitize_summary("Error in Vec: failed"), + "Error in Vec: failed" + ); + } + #[test] fn test_sanitize_summary_multibyte_truncation() { use super::sanitize_summary;