aether-agent-cli 0.9.13

CLI and ACP server for the Aether AI coding agent
Documentation
use aether_core::agent_spec::AgentSpec;
use aether_project::{AetherSettings, AgentCatalog, SettingsError};
use llm::{ProviderConnectionOverrides, ReasoningEffort};
use std::path::Path;
use thiserror::Error;

const FALLBACK_MODEL: &str = "anthropic:claude-sonnet-4-5";

#[derive(Clone, Debug, Default)]
pub(crate) enum InitialSessionSelection {
    #[default]
    Default,
    Agent(String),
    Model {
        model: String,
        reasoning_effort: Option<ReasoningEffort>,
    },
}

impl InitialSessionSelection {
    pub(crate) fn agent(name: String) -> Self {
        Self::Agent(name)
    }

    pub(crate) fn model(model: String, reasoning_effort: Option<ReasoningEffort>) -> Self {
        Self::Model { model, reasoning_effort }
    }
}

pub(crate) struct ResolvedAgentSelection {
    pub(crate) spec: AgentSpec,
    pub(crate) catalog: AgentCatalog,
}

#[derive(Debug, Error)]
pub(crate) enum AgentSelectionError {
    #[error(transparent)]
    Settings(#[from] SettingsError),
    #[error("{0}")]
    Agent(SettingsError),
    #[error("Model error: {0}")]
    Model(String),
}

pub(crate) fn resolve_agent_from_settings(
    cwd: &Path,
    settings: AetherSettings,
    provider_connections: ProviderConnectionOverrides,
    selection: &InitialSessionSelection,
) -> Result<ResolvedAgentSelection, AgentSelectionError> {
    let catalog = AgentCatalog::from_settings_or_empty(cwd, settings)?.with_provider_connections(provider_connections);
    resolve_agent_from_catalog(catalog, selection)
}

pub(crate) fn resolve_agent_from_catalog(
    catalog: AgentCatalog,
    selection: &InitialSessionSelection,
) -> Result<ResolvedAgentSelection, AgentSelectionError> {
    let spec = match selection {
        InitialSessionSelection::Agent(name) => catalog.resolve(name).map_err(AgentSelectionError::Agent)?,
        InitialSessionSelection::Model { model, reasoning_effort } => {
            catalog.default_spec(&model.parse().map_err(AgentSelectionError::Model)?, *reasoning_effort)
        }
        InitialSessionSelection::Default => match catalog.default_agent() {
            Some(spec) => spec.clone(),
            None => catalog.default_spec(&FALLBACK_MODEL.parse().map_err(AgentSelectionError::Model)?, None),
        },
    };

    Ok(ResolvedAgentSelection { spec, catalog })
}

pub fn resolve_agent_spec(
    catalog: &AgentCatalog,
    agent_name: Option<&str>,
) -> Result<AgentSpec, crate::error::CliError> {
    let selection =
        agent_name.map_or(InitialSessionSelection::Default, |name| InitialSessionSelection::Agent(name.to_string()));
    resolve_agent_from_catalog(catalog.clone(), &selection).map(|resolved| resolved.spec).map_err(|error| match error {
        AgentSelectionError::Settings(error) | AgentSelectionError::Agent(error) => {
            crate::error::CliError::AgentError(error.to_string())
        }
        AgentSelectionError::Model(error) => crate::error::CliError::ModelError(error),
    })
}