rho-coding-agent 1.8.2

A lightweight agent harness inspired by Pi
Documentation
use super::{
    build_sdk_provider, config_picker, App, ComposerMode, Entry, InteractiveRuntime, PickerAction,
};
use rho_providers::{
    model::{
        models_dev, ModelMetadata, ReasoningCapabilities, ReasoningRequestSource,
        ReasoningResolution,
    },
    reasoning::ReasoningLevel,
};

pub(super) struct FetchedReasoningResolution {
    pub(super) effective: ReasoningLevel,
    pub(super) rejected: Option<ReasoningLevel>,
}

impl super::TuiBootstrap {
    pub(super) fn set_reasoning(&mut self, level: ReasoningLevel, source: ReasoningRequestSource) {
        self.runtime.reasoning = level;
        self.runtime.reasoning_source = source;
        self.services.diagnostics.update_identity(
            &self.runtime.provider,
            &self.runtime.model,
            level,
        );
    }
}

pub(super) fn cached_metadata(provider: &str, model: &str) -> Option<(ModelMetadata, bool)> {
    let metadata = models_dev::cached_model_metadata(provider, model)?;
    let is_current = !models_dev::model_metadata_needs_refresh(provider, model);
    Some((metadata, is_current))
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct ModelSwitchReasoningResolution {
    pub(super) effective: ReasoningLevel,
    pub(super) source: ReasoningRequestSource,
}

pub(super) fn resolve_model_switch_reasoning(
    capabilities: &ReasoningCapabilities,
    requested: ReasoningLevel,
    source: ReasoningRequestSource,
) -> Result<ModelSwitchReasoningResolution, ReasoningLevel> {
    let resolution = capabilities.resolve(requested, source);
    match resolution {
        ReasoningResolution::UnsupportedExplicit(requested) => Err(requested),
        ReasoningResolution::Normalized { effective, .. } => Ok(ModelSwitchReasoningResolution {
            effective,
            source: ReasoningRequestSource::PersistedOrDefault,
        }),
        ReasoningResolution::Exact(effective) | ReasoningResolution::Unknown(effective) => {
            Ok(ModelSwitchReasoningResolution { effective, source })
        }
        ReasoningResolution::NotConfigurable => Ok(ModelSwitchReasoningResolution {
            effective: requested,
            source,
        }),
    }
}

pub(super) fn resolve_fetched_reasoning(
    capabilities: &ReasoningCapabilities,
    current: ReasoningLevel,
    at_fetch_start: Option<(ReasoningLevel, ReasoningRequestSource)>,
) -> FetchedReasoningResolution {
    let source = match at_fetch_start {
        Some((reasoning, _)) if reasoning != current => ReasoningRequestSource::Explicit,
        Some((_, source)) => source,
        None => ReasoningRequestSource::PersistedOrDefault,
    };
    let resolution = capabilities.resolve(current, source);
    if let ReasoningResolution::UnsupportedExplicit(requested) = resolution {
        let effective = at_fetch_start
            .and_then(|(reasoning, _)| {
                capabilities
                    .resolve(reasoning, ReasoningRequestSource::PersistedOrDefault)
                    .effective()
            })
            .unwrap_or(current);
        return FetchedReasoningResolution {
            effective,
            rejected: Some(requested),
        };
    }
    FetchedReasoningResolution {
        effective: resolution.effective().unwrap_or(current),
        rejected: None,
    }
}

impl App {
    pub(super) fn cycle_reasoning(&mut self, agent: &mut InteractiveRuntime) -> anyhow::Result<()> {
        let capabilities = models_dev::current_reasoning_capabilities(
            &self.info.runtime.provider,
            &self.info.runtime.model,
        );
        if capabilities == ReasoningCapabilities::NotConfigurable {
            return Ok(());
        }
        let reasoning = capabilities.next_level(self.info.runtime.reasoning);
        let provider = match build_sdk_provider(
            &self.info.runtime.provider,
            &self.info.runtime.model,
            reasoning,
        ) {
            Ok(provider) => provider,
            Err(err) => {
                self.insert_entry(&Entry::Error(format!(
                    "could not update reasoning to {reasoning}: {err}"
                )));
                self.status = "reasoning change failed".into();
                return Ok(());
            }
        };
        agent.replace_provider(provider, reasoning)?;
        self.info
            .set_reasoning(reasoning, ReasoningRequestSource::Explicit);
        let save_result = self.info.services.config_repository.update(|config| {
            config.reasoning = reasoning;
        });
        if matches!(
            &self.composer,
            ComposerMode::Picker(picker) if picker.action == PickerAction::Config
        ) {
            let config = self
                .info
                .services
                .config_repository
                .load()
                .unwrap_or_default();
            self.info.runtime.show_reasoning_output = config.show_reasoning_output;
            self.refresh_main_config_picker(config_picker::REASONING_VALUE)?;
        }
        match save_result {
            Ok(()) => self.status = format!("reasoning: {reasoning}"),
            Err(err) => {
                self.insert_entry(&Entry::Error(format!(
                    "reasoning set to {reasoning} for this session, but saving config failed: {err}"
                )));
                self.status = "config save failed".into();
            }
        }
        Ok(())
    }
}

#[cfg(test)]
#[path = "reasoning_metadata_tests.rs"]
mod tests;