feat(engine): reasoning_effort auto mode (#669)

This commit is contained in:
Hunter Bown
2026-05-05 00:11:56 -05:00
5 changed files with 134 additions and 1 deletions
+70
View File
@@ -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);
}
}
+4
View File
@@ -199,6 +199,7 @@ pub enum ReasoningEffortValue {
Low,
Medium,
High,
Auto,
Max,
}
@@ -689,6 +690,7 @@ impl From<ReasoningEffort> 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<ReasoningEffortValue> for ReasoningEffort {
ReasoningEffortValue::Low => Self::Low,
ReasoningEffortValue::Medium => Self::Medium,
ReasoningEffortValue::High => Self::High,
ReasoningEffortValue::Auto => Self::Auto,
ReasoningEffortValue::Max => Self::Max,
}
}
+54 -1
View File
@@ -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<String> {
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::<Vec<&str>>()
.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,
}
}
+1
View File
@@ -13,6 +13,7 @@ use tempfile::NamedTempFile;
use wait_timeout::ChildExt;
mod audit;
mod auto_reasoning;
mod automation_manager;
mod client;
mod command_safety;
+5
View File
@@ -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,
}