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)?(?:{})(?:\s[^>]*)?>", 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;