feat(engine): reasoning_effort auto mode (#669)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use tempfile::NamedTempFile;
|
||||
use wait_timeout::ChildExt;
|
||||
|
||||
mod audit;
|
||||
mod auto_reasoning;
|
||||
mod automation_manager;
|
||||
mod client;
|
||||
mod command_safety;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user