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 18071ef5..7c27fced 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.messages_with_turn_metadata(), @@ -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, @@ -1642,3 +1649,49 @@ impl Engine { messages } } + +/// 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 445d8110..f2d2e967 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 29f52073..0c1d0967 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -100,6 +100,7 @@ pub enum ReasoningEffort { Low, Medium, High, + Auto, #[default] Max, } @@ -114,6 +115,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(), } @@ -127,6 +129,7 @@ impl ReasoningEffort { Self::Low => "low", Self::Medium => "medium", Self::High => "high", + Self::Auto => "auto", Self::Max => "max", } } @@ -139,6 +142,7 @@ impl ReasoningEffort { Self::Low => "low", Self::Medium => "med", Self::High => "high", + Self::Auto => "auto", Self::Max => "max", } } @@ -156,6 +160,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, }