use std::future::Future;
use crate::{agent::ADVISOR_AGENT_ID, config::InternalAgentModelConfig};
use super::{
advisor_status::AdvisorStatus, agent_picker::InternalAgentModelPickerOrigin, App,
CommandInvocation, ComposerMode, Entry, InteractiveRuntime,
};
const SELECT_ADVISOR_MODEL_STATUS: &str = "select an advisor model to turn advisor mode on";
const SELECT_ADVISOR_MODEL_EDIT_STATUS: &str = "select an advisor model";
pub(super) trait AdvisorRuntime {
fn set_advisor(
&mut self,
model: Option<InternalAgentModelConfig>,
) -> impl Future<Output = anyhow::Result<Option<String>>> + Send;
fn tool_specs(&self) -> Vec<rho_sdk::model::ToolSpec>;
}
impl AdvisorRuntime for InteractiveRuntime {
fn set_advisor(
&mut self,
model: Option<InternalAgentModelConfig>,
) -> impl Future<Output = anyhow::Result<Option<String>>> + Send {
InteractiveRuntime::set_advisor(self, model)
}
fn tool_specs(&self) -> Vec<rho_sdk::model::ToolSpec> {
InteractiveRuntime::tool_specs(self)
}
}
impl App {
pub(super) async fn execute_advisor_command(
&mut self,
invocation: CommandInvocation,
agent: &mut InteractiveRuntime,
) -> anyhow::Result<()> {
self.execute_advisor_command_with_runtime(invocation, agent)
.await
}
async fn execute_advisor_command_with_runtime(
&mut self,
invocation: CommandInvocation,
agent: &mut impl AdvisorRuntime,
) -> anyhow::Result<()> {
let requested = match invocation.args.trim().to_ascii_lowercase().as_str() {
"" => !self.info.runtime.advisor_mode,
"on" => true,
"off" => false,
_ => {
self.insert_entry(&Entry::Error("usage: /advisor [on|off]".into()));
self.set_status("invalid advisor mode");
return Ok(());
}
};
if requested && !self.advisor_model_configured() {
self.open_advisor_model_prompt(InternalAgentModelPickerOrigin::AdvisorCommand);
return Ok(());
}
self.set_advisor_mode(requested, agent).await
}
pub(super) fn advisor_model_configured(&self) -> bool {
self.info
.runtime
.internal_agents
.contains_key(ADVISOR_AGENT_ID)
}
pub(super) fn open_advisor_model_prompt(&mut self, origin: InternalAgentModelPickerOrigin) {
if self.open_internal_agent_model_picker(ADVISOR_AGENT_ID, origin) {
let status = match origin {
InternalAgentModelPickerOrigin::AdvisorModelConfigRow => {
SELECT_ADVISOR_MODEL_EDIT_STATUS
}
_ => SELECT_ADVISOR_MODEL_STATUS,
};
self.set_status(status);
}
}
pub(super) async fn finish_advisor_model_selection(
&mut self,
selected: bool,
agent: &mut impl AdvisorRuntime,
) -> anyhow::Result<()> {
if !selected {
return Ok(());
}
self.set_advisor_mode(true, agent).await
}
pub(super) fn cancel_advisor_model_prompt(&mut self) -> bool {
let pending = matches!(
self.internal_agent_model_target.as_ref(),
Some(target) if target.origin == InternalAgentModelPickerOrigin::AdvisorCommand
);
if pending {
self.internal_agent_model_target = None;
self.input_ui.set_composer(ComposerMode::Input);
self.set_status("advisor mode stays off: no advisor model selected");
}
pending
}
pub(super) async fn set_advisor_mode(
&mut self,
enabled: bool,
agent: &mut impl AdvisorRuntime,
) -> anyhow::Result<()> {
let previous_mode = self.info.runtime.advisor_mode;
let previous_model = self
.info
.runtime
.internal_agents
.get(ADVISOR_AGENT_ID)
.cloned();
let desired_model = enabled.then(|| previous_model.clone()).flatten();
let notice = match agent.set_advisor(desired_model).await {
Ok(notice) => notice,
Err(error) => {
self.insert_entry(&Entry::Error(format!(
"advisor mode could not be applied to this session: {error}"
)));
self.set_status("advisor mode change failed");
return Err(error);
}
};
if previous_mode != enabled {
if let Err(error) = self
.info
.services
.config_repository
.update(|config| config.advisor_mode = enabled)
{
let rollback_model = previous_mode.then_some(previous_model).flatten();
if let Err(rollback_error) = agent.set_advisor(rollback_model).await {
self.insert_entry(&Entry::Error(format!(
"could not save advisor mode: {error}; runtime rollback failed: {rollback_error}"
)));
} else {
self.insert_entry(&Entry::Error(format!(
"could not save advisor mode: {error}"
)));
}
self.set_status("config save failed");
return Ok(());
}
self.info.runtime.advisor_mode = enabled;
}
self.info
.services
.diagnostics
.update_advisor_mode(self.info.runtime.advisor_mode);
if let Some(display) = notice {
self.insert_entry(&Entry::Notice(display));
self.info
.services
.diagnostics
.update_tools(&agent.tool_specs());
}
self.statusline.update_model(&self.info.runtime);
let status = self.advisor_mode_status();
self.set_status(status);
Ok(())
}
pub(super) fn set_advisor_reasoning(
&mut self,
reasoning: rho_providers::reasoning::ReasoningLevel,
) -> anyhow::Result<()> {
let Some(mut selection) = self
.info
.runtime
.internal_agents
.get(ADVISOR_AGENT_ID)
.cloned()
else {
self.set_status("select an advisor model first");
return Ok(());
};
selection.reasoning = Some(reasoning);
self.info
.runtime
.internal_agents
.insert(ADVISOR_AGENT_ID.into(), selection.clone());
match self.info.services.config_repository.update(|config| {
config.set_internal_agent_model_config(ADVISOR_AGENT_ID, selection);
}) {
Ok(()) => self.set_status(format!("advisor reasoning: {reasoning}")),
Err(err) => {
self.insert_entry(&Entry::Error(format!(
"advisor reasoning set to {reasoning} for this session, but saving config failed: {err}"
)));
self.set_status("config save failed");
}
}
Ok(())
}
pub(super) async fn sync_advisor_runtime(&mut self, agent: &mut impl AdvisorRuntime) {
let model = self.info.runtime.advisor_mode.then(|| {
self.info
.runtime
.internal_agents
.get(ADVISOR_AGENT_ID)
.cloned()
});
self.info
.services
.diagnostics
.update_advisor_mode(self.info.runtime.advisor_mode);
match agent.set_advisor(model.flatten()).await {
Ok(Some(display)) => {
self.insert_entry(&Entry::Notice(display));
self.info
.services
.diagnostics
.update_tools(&agent.tool_specs());
}
Ok(None) => {}
Err(error) => {
self.insert_entry(&Entry::Error(format!(
"advisor mode could not be applied to this session: {error}"
)));
}
}
}
pub(super) fn advisor_mode_status(&self) -> String {
match AdvisorStatus::from_runtime(&self.info.runtime) {
AdvisorStatus::Off => "advisor mode is off".into(),
AdvisorStatus::Reviewing { model } => {
format!("advisor mode is on: {model} reviews the session")
}
AdvisorStatus::MissingModel => {
"advisor mode is on, but no advisor model is selected".into()
}
}
}
}
#[cfg(test)]
#[path = "advisor_command_tests.rs"]
mod tests;