| 1 | //! Adaptive reasoning-effort tier selection for `Auto` mode (#663). |
| 2 | //! |
| 3 | //! When the user sets `reasoning_effort = "auto"`, the engine calls |
| 4 | //! [`select`] before each turn-level request to pick the actual tier |
| 5 | //! based on the current message. |
| 6 | |
| 7 | use crate::tui::app::ReasoningEffort; |
| 8 | |
| 9 | /// Choose a concrete `ReasoningEffort` tier for the next API request. |
| 10 | /// |
| 11 | /// Rules: |
| 12 | /// - Sub-agent contexts (`is_subagent == true`) → `Low` |
| 13 | /// - Last user message contains a high-effort keyword |
| 14 | /// (English: `debug`, `error`; Chinese: 调试 / 错误 / 报错 / 出错 / |
| 15 | /// 崩溃 / 調試 / 錯誤; Japanese: デバッグ / エラー / バグ) → `Max` |
| 16 | /// - Last user message contains a low-effort keyword |
| 17 | /// (English: `search`, `lookup`; Chinese: 搜索 / 查找 / 查询; |
| 18 | /// Japanese: 検索) → `Low` |
| 19 | /// - Everything else → `High` |
| 20 | #[must_use] |
| 21 | pub fn select(is_subagent: bool, last_msg: &str) -> ReasoningEffort { |
| 22 | if is_subagent { |
| 23 | return ReasoningEffort::Low; |
| 24 | } |
| 25 | |
| 26 | let lower = last_msg.to_ascii_lowercase(); |
| 27 | |
| 28 | if HIGH_EFFORT_KEYWORDS.iter().any(|kw| lower.contains(kw)) { |
| 29 | return ReasoningEffort::Max; |
| 30 | } |
| 31 | |
| 32 | if LOW_EFFORT_KEYWORDS.iter().any(|kw| lower.contains(kw)) { |
| 33 | return ReasoningEffort::Low; |
| 34 | } |
| 35 | |
| 36 | ReasoningEffort::High |
| 37 | } |
| 38 | |
| 39 | /// Keywords that bump `reasoning_effort` to `Max`. Latin terms are |
| 40 | /// lowercase because the caller lowercases the message; CJK has no |
| 41 | /// case so the literal form matches as-is. Covers the Chinese and |
| 42 | /// Japanese vocabulary a non-English user reaches for when reporting |
| 43 | /// the same kind of problem the original `"debug" | "error"` rule was |
| 44 | /// trying to catch — without those terms a Chinese-speaking user |
| 45 | /// paying for Auto mode silently got `High` even on hard debugging |
| 46 | /// tasks. |
| 47 | const HIGH_EFFORT_KEYWORDS: &[&str] = &[ |
| 48 | // English (unchanged from the original keyword set). |
| 49 | "debug", |
| 50 | "error", |
| 51 | // Simplified / Traditional Chinese. |
| 52 | "\u{8c03}\u{8bd5}", // 调试 |
| 53 | "\u{9519}\u{8bef}", // 错误 |
| 54 | "\u{62a5}\u{9519}", // 报错 |
| 55 | "\u{51fa}\u{9519}", // 出错 |
| 56 | "\u{5d29}\u{6e83}", // 崩溃 |
| 57 | "\u{8abf}\u{8a66}", // 調試 |
| 58 | "\u{932f}\u{8aa4}", // 錯誤 |
| 59 | // Japanese. |
| 60 | "\u{30c7}\u{30d0}\u{30c3}\u{30b0}", // デバッグ |
| 61 | "\u{30a8}\u{30e9}\u{30fc}", // エラー |
| 62 | "\u{30d0}\u{30b0}", // バグ |
| 63 | ]; |
| 64 | |
| 65 | /// Keywords that drop `reasoning_effort` to `Low`. Same locale coverage |
| 66 | /// as [`HIGH_EFFORT_KEYWORDS`]. |
| 67 | const LOW_EFFORT_KEYWORDS: &[&str] = &[ |
| 68 | "search", |
| 69 | "lookup", |
| 70 | "\u{641c}\u{7d22}", // 搜索 |
| 71 | "\u{67e5}\u{627e}", // 查找 |
| 72 | "\u{67e5}\u{8be2}", // 查询 |
| 73 | "\u{691c}\u{7d22}", // 検索 |
| 74 | ]; |
| 75 | |
| 76 | #[cfg(test)] |
| 77 | mod tests { |
| 78 | use super::*; |
| 79 | |
| 80 | #[test] |
| 81 | fn subagent_returns_low() { |
| 82 | assert_eq!(select(true, "anything"), ReasoningEffort::Low); |
| 83 | assert_eq!(select(true, "debug this"), ReasoningEffort::Low); |
| 84 | assert_eq!(select(true, "search query"), ReasoningEffort::Low); |
| 85 | } |
| 86 | |
| 87 | #[test] |
| 88 | fn debug_or_error_returns_max() { |
| 89 | assert_eq!(select(false, "find a bug"), ReasoningEffort::High); |
| 90 | assert_eq!(select(false, "debug crash"), ReasoningEffort::Max); |
| 91 | assert_eq!(select(false, "Error: timeout"), ReasoningEffort::Max); |
| 92 | assert_eq!(select(false, "fix this error"), ReasoningEffort::Max); |
| 93 | assert_eq!(select(false, "DEBUG output"), ReasoningEffort::Max); |
| 94 | } |
| 95 | |
| 96 | #[test] |
| 97 | fn search_or_lookup_returns_low() { |
| 98 | assert_eq!(select(false, "search for the file"), ReasoningEffort::Low); |
| 99 | assert_eq!(select(false, "lookup docs"), ReasoningEffort::Low); |
| 100 | assert_eq!(select(false, "SearchQuery"), ReasoningEffort::Low); |
| 101 | assert_eq!(select(false, "lookup_user"), ReasoningEffort::Low); |
| 102 | } |
| 103 | |
| 104 | #[test] |
| 105 | fn default_returns_high() { |
| 106 | assert_eq!(select(false, "hello"), ReasoningEffort::High); |
| 107 | assert_eq!(select(false, "write a test"), ReasoningEffort::High); |
| 108 | assert_eq!(select(false, "refactor this module"), ReasoningEffort::High); |
| 109 | assert_eq!(select(false, ""), ReasoningEffort::High); |
| 110 | } |
| 111 | |
| 112 | #[test] |
| 113 | fn chinese_debug_keywords_return_max() { |
| 114 | // The original keyword set was English-only; Chinese-speaking |
| 115 | // Auto-mode users paid for `High` even on real debugging tasks. |
| 116 | for msg in [ |
| 117 | "\u{5e2e}\u{6211}\u{8c03}\u{8bd5}\u{4ee3}\u{7801}", // 帮我调试代码 |
| 118 | "\u{8fd9}\u{91cc}\u{6709}\u{4e2a}\u{9519}\u{8bef}", // 这里有个错误 |
| 119 | "\u{4ee3}\u{7801}\u{62a5}\u{9519}\u{4e86}", // 代码报错了 |
| 120 | "\u{7a0b}\u{5e8f}\u{51fa}\u{9519}", // 程序出错 |
| 121 | "\u{7cfb}\u{7edf}\u{5d29}\u{6e83}", // 系统崩溃 |
| 122 | "\u{4ee3}\u{78bc}\u{8abf}\u{8a66}", // 代碼調試 (zh-Hant) |
| 123 | "\u{6709}\u{500b}\u{932f}\u{8aa4}", // 有個錯誤 (zh-Hant) |
| 124 | ] { |
| 125 | assert_eq!( |
| 126 | select(false, msg), |
| 127 | ReasoningEffort::Max, |
| 128 | "expected Max for `{msg}`", |
| 129 | ); |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | #[test] |
| 134 | fn japanese_debug_keywords_return_max() { |
| 135 | for msg in [ |
| 136 | "\u{30b3}\u{30fc}\u{30c9}\u{3092}\u{30c7}\u{30d0}\u{30c3}\u{30b0}", // コードをデバッグ |
| 137 | "\u{30a8}\u{30e9}\u{30fc}\u{304c}\u{51fa}\u{305f}", // エラーが出た |
| 138 | "\u{30d0}\u{30b0}\u{3092}\u{4fee}\u{6b63}", // バグを修正 |
| 139 | ] { |
| 140 | assert_eq!( |
| 141 | select(false, msg), |
| 142 | ReasoningEffort::Max, |
| 143 | "expected Max for `{msg}`", |
| 144 | ); |
| 145 | } |
| 146 | } |
| 147 | |
| 148 | #[test] |
| 149 | fn chinese_search_keywords_return_low() { |
| 150 | for msg in [ |
| 151 | "\u{641c}\u{7d22}\u{4e00}\u{4e0b}\u{6587}\u{4ef6}", // 搜索一下文件 |
| 152 | "\u{5e2e}\u{6211}\u{67e5}\u{627e}\u{5b9a}\u{4e49}", // 帮我查找定义 |
| 153 | "\u{67e5}\u{8be2}\u{6587}\u{6863}", // 查询文档 |
| 154 | ] { |
| 155 | assert_eq!( |
| 156 | select(false, msg), |
| 157 | ReasoningEffort::Low, |
| 158 | "expected Low for `{msg}`", |
| 159 | ); |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | #[test] |
| 164 | fn japanese_search_keyword_returns_low() { |
| 165 | // 検索 → "search" |
| 166 | assert_eq!( |
| 167 | select( |
| 168 | false, |
| 169 | "\u{30c9}\u{30ad}\u{30e5}\u{30e1}\u{30f3}\u{30c8}\u{691c}\u{7d22}" |
| 170 | ), |
| 171 | ReasoningEffort::Low, |
| 172 | ); |
| 173 | } |
| 174 | |
| 175 | #[test] |
| 176 | fn cjk_default_still_returns_high() { |
| 177 | // No keyword hits — ordinary Chinese/Japanese prose stays on |
| 178 | // the `High` default like English does. |
| 179 | for msg in [ |
| 180 | "\u{5e2e}\u{6211}\u{5199}\u{4e2a}\u{6d4b}\u{8bd5}", // 帮我写个测试 |
| 181 | "\u{91cd}\u{6784}\u{8fd9}\u{4e2a}\u{6a21}\u{5757}", // 重构这个模块 |
| 182 | "\u{30c6}\u{30b9}\u{30c8}\u{3092}\u{66f8}\u{304f}", // テストを書く |
| 183 | ] { |
| 184 | assert_eq!( |
| 185 | select(false, msg), |
| 186 | ReasoningEffort::High, |
| 187 | "expected High for `{msg}`", |
| 188 | ); |
| 189 | } |
| 190 | } |
| 191 | } |
| 192 |