use std::sync::Arc;
use agent_client_protocol::{
schema::v1::{
SessionConfigOption, SessionConfigOptionCategory, SessionConfigSelectOption, SessionId,
SetSessionConfigOptionRequest,
},
Error as AcpError,
};
use rho_providers::{
model::{
models_dev::cached_reasoning_capabilities, ReasoningCapabilities, ReasoningRequestSource,
ReasoningResolution,
},
reasoning::ReasoningLevel,
};
use crate::{
app::{
runtime_builder::{configured_context_window, refresh_session_compaction},
session_assembly::BuiltSession,
},
compaction::CompactionConfig,
config::Config,
};
pub(super) const THOUGHT_LEVEL_ID: &str = "thought_level";
pub(super) fn config_options(config: &Config, current: ReasoningLevel) -> Vec<SessionConfigOption> {
match thought_capabilities(config) {
ReasoningCapabilities::NotConfigurable => Vec::new(),
ReasoningCapabilities::Unknown | ReasoningCapabilities::Levels(_) => {
vec![thought_level_option(config, current)]
}
}
}
pub(super) fn thought_level_option(
config: &Config,
current: ReasoningLevel,
) -> SessionConfigOption {
let choices = selectable_thought_levels(config, current)
.into_iter()
.map(|level| {
let id = level.to_string();
SessionConfigSelectOption::new(id.clone(), id)
})
.collect::<Vec<_>>();
SessionConfigOption::select(THOUGHT_LEVEL_ID, "Reasoning", current.to_string(), choices)
.description("How much reasoning the model spends on each turn.")
.category(SessionConfigOptionCategory::ThoughtLevel)
}
pub(super) fn selectable_thought_levels(
config: &Config,
current: ReasoningLevel,
) -> Vec<ReasoningLevel> {
thought_capabilities(config).selectable_levels(&ReasoningLevel::ALL, Some(current))
}
pub(super) fn parse_thought_level_request(
request: &SetSessionConfigOptionRequest,
) -> Result<ReasoningLevel, AcpError> {
if request.config_id.0.as_ref() != THOUGHT_LEVEL_ID {
return Err(AcpError::invalid_params().data(format!(
"unknown session config option '{}'",
request.config_id.0.as_ref()
)));
}
let Some(value) = request.value.as_value_id() else {
return Err(
AcpError::invalid_params().data("thought_level requires a select value id".to_string())
);
};
value.0.as_ref().parse::<ReasoningLevel>().map_err(|_| {
AcpError::invalid_params().data(format!("unknown thought_level '{}'", value.0.as_ref()))
})
}
pub(super) fn resolve_thought_level(
config: &Config,
requested: ReasoningLevel,
) -> Result<ReasoningLevel, AcpError> {
let capabilities = thought_capabilities(config);
match capabilities.resolve(requested, ReasoningRequestSource::Explicit) {
ReasoningResolution::Exact(level) | ReasoningResolution::Unknown(level) => Ok(level),
ReasoningResolution::NotConfigurable => Err(AcpError::invalid_params().data(format!(
"provider '{}' model '{}' does not expose configurable reasoning",
config.provider, config.model
))),
ReasoningResolution::UnsupportedExplicit(requested) => {
let supported = capabilities
.levels()
.map(|levels| {
levels
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
})
.unwrap_or_else(|| "none".to_string());
Err(AcpError::invalid_params().data(format!(
"provider '{}' model '{}' does not support reasoning level '{}'; supported levels: {}",
config.provider, config.model, requested, supported
)))
}
ReasoningResolution::Normalized { .. } => Err(AcpError::internal_error()
.data("explicit thought_level must not be auto-normalized".to_string())),
}
}
pub(super) fn apply_thought_level(
built: &BuiltSession,
config: &Config,
requested: ReasoningLevel,
) -> Result<(), AcpError> {
let current = built.session.reasoning_level();
if requested == current {
return Ok(());
}
let level = resolve_thought_level(config, requested)?;
built
.session
.set_reasoning_level(level)
.map_err(|error| map_session_error(&built.session, error))?;
if let Err(error) = refresh_session_compaction(
&built.session,
Arc::clone(&built.provider),
built.tools.tools(),
level,
CompactionConfig::from(config),
configured_context_window(config),
built.runtime.usage_recording(),
) {
let _ = built.session.set_reasoning_level(current);
return Err(map_session_error(&built.session, error));
}
Ok(())
}
fn thought_capabilities(config: &Config) -> ReasoningCapabilities {
cached_reasoning_capabilities(&config.provider, &config.model)
}
fn map_session_error(session: &rho_sdk::Session, error: rho_sdk::Error) -> AcpError {
match error {
rho_sdk::Error::SessionBusy => {
super::agent::busy_session(&SessionId::new(session.id().as_str()))
}
error => AcpError::internal_error().data(error.to_string()),
}
}
#[cfg(test)]
#[path = "thought_level_tests.rs"]
mod tests;