From e5b862540f350794db6881c88ec2c32285679054 Mon Sep 17 00:00:00 2001 From: wangfeng Date: Mon, 4 May 2026 18:08:19 -0700 Subject: [PATCH] feat(engine): reasoning_effort auto mode (closes #663) --- crates/tui/src/auto_reasoning.rs | 70 +++++++++++++++++++++++++ crates/tui/src/config_ui.rs | 4 ++ crates/tui/src/core/engine/turn_loop.rs | 55 ++++++++++++++++++- crates/tui/src/main.rs | 1 + crates/tui/src/tui/app.rs | 5 ++ 5 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 crates/tui/src/auto_reasoning.rs diff --git a/crates/tui/src/auto_reasoning.rs b/crates/tui/src/auto_reasoning.rs new file mode 100644 index 00000000..8c2a2fc0 --- /dev/null +++ b/crates/tui/src/auto_reasoning.rs @@ -0,0 +1,70 @@ +//! Adaptive reasoning-effort tier selection for `Auto` mode (#663). +//! +//! When the user sets `reasoning_effort = "auto"`, the engine calls +//! [`select`] before each turn-level request to pick the actual tier +//! based on the current message. + +use crate::tui::app::ReasoningEffort; + +/// Choose a concrete `ReasoningEffort` tier for the next API request. +/// +/// Rules: +/// - Sub-agent contexts (`is_subagent == true`) → `Low` +/// - Last user message contains `"debug"` or `"error"` → `Max` +/// - Last user message contains `"search"` or `"lookup"` → `Low` +/// - Everything else → `High` +#[must_use] +pub fn select(is_subagent: bool, last_msg: &str) -> ReasoningEffort { + if is_subagent { + return ReasoningEffort::Low; + } + + let lower = last_msg.to_ascii_lowercase(); + + if lower.contains("debug") || lower.contains("error") { + return ReasoningEffort::Max; + } + + if lower.contains("search") || lower.contains("lookup") { + return ReasoningEffort::Low; + } + + ReasoningEffort::High +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn subagent_returns_low() { + assert_eq!(select(true, "anything"), ReasoningEffort::Low); + assert_eq!(select(true, "debug this"), ReasoningEffort::Low); + assert_eq!(select(true, "search query"), ReasoningEffort::Low); + } + + #[test] + fn debug_or_error_returns_max() { + assert_eq!(select(false, "find a bug"), ReasoningEffort::High); + assert_eq!(select(false, "debug crash"), ReasoningEffort::Max); + assert_eq!(select(false, "Error: timeout"), ReasoningEffort::Max); + assert_eq!(select(false, "fix this error"), ReasoningEffort::Max); + assert_eq!(select(false, "DEBUG output"), ReasoningEffort::Max); + } + + #[test] + fn search_or_lookup_returns_low() { + assert_eq!(select(false, "search for the file"), ReasoningEffort::Low); + assert_eq!(select(false, "lookup docs"), ReasoningEffort::Low); + assert_eq!(select(false, "SearchQuery"), ReasoningEffort::Low); + assert_eq!(select(false, "lookup_user"), ReasoningEffort::Low); + } + + #[test] + fn default_returns_high() { + assert_eq!(select(false, "hello"), ReasoningEffort::High); + assert_eq!(select(false, "write a test"), ReasoningEffort::High); + assert_eq!(select(false, "refactor this module"), ReasoningEffort::High); + assert_eq!(select(false, ""), ReasoningEffort::High); + } +} diff --git a/crates/tui/src/config_ui.rs b/crates/tui/src/config_ui.rs index 5d9a14fe..4472bcd3 100644 --- a/crates/tui/src/config_ui.rs +++ b/crates/tui/src/config_ui.rs @@ -199,6 +199,7 @@ pub enum ReasoningEffortValue { Low, Medium, High, + Auto, Max, } @@ -689,6 +690,7 @@ impl From for ReasoningEffortValue { ReasoningEffort::Low => Self::Low, ReasoningEffort::Medium => Self::Medium, ReasoningEffort::High => Self::High, + ReasoningEffort::Auto => Self::Auto, ReasoningEffort::Max => Self::Max, } } @@ -701,6 +703,7 @@ impl ReasoningEffortValue { ReasoningEffort::Low => Self::Low, ReasoningEffort::Medium => Self::Medium, ReasoningEffort::High => Self::High, + ReasoningEffort::Auto => Self::Auto, ReasoningEffort::Max => Self::Max, } } @@ -713,6 +716,7 @@ impl From for ReasoningEffort { ReasoningEffortValue::Low => Self::Low, ReasoningEffortValue::Medium => Self::Medium, ReasoningEffortValue::High => Self::High, + ReasoningEffortValue::Auto => Self::Auto, ReasoningEffortValue::Max => Self::Max, } } diff --git a/crates/tui/src/core/engine/turn_loop.rs b/crates/tui/src/core/engine/turn_loop.rs index 603309d0..762aeaf1 100644 --- a/crates/tui/src/core/engine/turn_loop.rs +++ b/crates/tui/src/core/engine/turn_loop.rs @@ -228,6 +228,13 @@ impl Engine { force_update_plan_this_step, )) }; + + // Resolve `auto` reasoning_effort to a concrete tier (#663). + let effective_reasoning_effort = resolve_auto_effort( + self.session.reasoning_effort.as_deref(), + &self.session.messages, + ); + let request = MessageRequest { model: self.session.model.clone(), messages: self.session.messages.clone(), @@ -241,7 +248,7 @@ impl Engine { }, metadata: None, thinking: None, - reasoning_effort: self.session.reasoning_effort.clone(), + reasoning_effort: effective_reasoning_effort, stream: Some(true), temperature: None, top_p: None, @@ -1595,3 +1602,49 @@ impl Engine { (TurnOutcomeStatus::Completed, None) } } + +/// Resolve an `"auto"` reasoning-effort tier to a concrete value. +/// +/// When the configured effort is `"auto"`, inspects the last user message +/// and calls [`crate::auto_reasoning::select`] to pick the actual tier. +/// Non-`"auto"` values pass through unchanged. +fn resolve_auto_effort(reasoning_effort: Option<&str>, messages: &[Message]) -> Option { + match reasoning_effort { + Some("auto") => { + // Find the last user message in the conversation. + let last_msg = messages + .iter() + .rev() + .find(|m| m.role == "user") + .map(|m| { + m.content + .iter() + .filter_map(|block| { + if let ContentBlock::Text { text, .. } = block { + Some(text.as_str()) + } else { + None + } + }) + .collect::>() + .join(" ") + }) + .unwrap_or_default(); + + // is_subagent is false here — handle_deepseek_turn runs in the + // main engine (not a sub-agent's inner loop). Sub-agents have + // their own turn pass and can pass is_subagent=true when they + // call this function directly. + let tier = crate::auto_reasoning::select(false, &last_msg); + let resolved = tier.as_setting().to_string(); + tracing::debug!( + reasoning_effort = %resolved, + is_subagent = false, + "auto_reasoning: resolved auto tier from user message" + ); + Some(resolved) + } + Some(other) => Some(other.to_string()), + None => None, + } +} diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 40b664df..d45061ed 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -13,6 +13,7 @@ use tempfile::NamedTempFile; use wait_timeout::ChildExt; mod audit; +mod auto_reasoning; mod automation_manager; mod client; mod command_safety; diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index f1d274b8..4f124027 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -103,6 +103,7 @@ pub enum ReasoningEffort { Low, Medium, High, + Auto, #[default] Max, } @@ -117,6 +118,7 @@ impl ReasoningEffort { "low" | "minimal" => Self::Low, "medium" | "mid" => Self::Medium, "high" => Self::High, + "auto" | "automatic" => Self::Auto, "max" | "maximum" | "xhigh" => Self::Max, _ => Self::default(), } @@ -130,6 +132,7 @@ impl ReasoningEffort { Self::Low => "low", Self::Medium => "medium", Self::High => "high", + Self::Auto => "auto", Self::Max => "max", } } @@ -142,6 +145,7 @@ impl ReasoningEffort { Self::Low => "low", Self::Medium => "med", Self::High => "high", + Self::Auto => "auto", Self::Max => "max", } } @@ -159,6 +163,7 @@ impl ReasoningEffort { pub fn cycle_next(self) -> Self { match self { Self::Off => Self::High, + Self::Auto => Self::Off, Self::Low | Self::Medium | Self::High => Self::Max, Self::Max => Self::Off, }