use std::collections::HashMap;
use crate::agent::classifier::TaskCategory;
use crate::agent::custom::CustomAgentConfig;
use crate::cmd::agent_json_types::{GroupHoldJson, QuotaJson};
use crate::types::AgentKind;
pub fn command_installed(command: &str) -> bool {
let binary = command.split_whitespace().next().unwrap_or_default();
if binary.is_empty() {
return false;
}
std::process::Command::new("which")
.arg(binary)
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
pub fn get_agent_capabilities(
kind: AgentKind,
custom_config: Option<&CustomAgentConfig>,
) -> HashMap<String, i32> {
let mut caps = HashMap::new();
if let Some(config) = custom_config {
caps.insert(TaskCategory::Research.label().to_string(), config.capabilities.research);
caps.insert(TaskCategory::SimpleEdit.label().to_string(), config.capabilities.simple_edit);
caps.insert(TaskCategory::ComplexImpl.label().to_string(), config.capabilities.complex_impl);
caps.insert(TaskCategory::Frontend.label().to_string(), config.capabilities.frontend);
caps.insert(TaskCategory::Debugging.label().to_string(), config.capabilities.debugging);
caps.insert(TaskCategory::Testing.label().to_string(), config.capabilities.testing);
caps.insert(TaskCategory::Refactoring.label().to_string(), config.capabilities.refactoring);
caps.insert(TaskCategory::Documentation.label().to_string(), config.capabilities.documentation);
} else {
for category in &[
TaskCategory::Research,
TaskCategory::SimpleEdit,
TaskCategory::ComplexImpl,
TaskCategory::Frontend,
TaskCategory::Debugging,
TaskCategory::Testing,
TaskCategory::Refactoring,
TaskCategory::Documentation,
] {
let score = crate::agent::selection::AGENT_CAPABILITIES.iter()
.find(|(candidate, _)| *candidate == kind)
.and_then(|(_, scores)| scores.iter().find(|(candidate, _)| *candidate == *category))
.map(|(_, score)| *score)
.unwrap_or(1);
caps.insert(category.label().to_string(), score);
}
}
caps
}
pub(crate) fn build_quota_json(rlk: &AgentKind, custom_name: Option<&str>) -> QuotaJson {
let avail = crate::route_availability::availability(rlk, custom_name);
let quota = crate::agent::selection::quota_from(&avail);
let window = avail
.probe
.as_ref()
.and_then(|probe| crate::agent::selection::tightest_window(&probe.windows))
.map(|item| item.label.clone())
.filter(|label| !label.is_empty());
let held = avail.status == crate::route_availability::RouteStatus::Held;
let groups = if held {
Vec::new()
} else {
crate::rate_limit::active_group_holds(rlk, custom_name)
.into_iter()
.map(|(group, info)| GroupHoldJson {
group,
recovery_at: info.recovery_at,
message: info.message,
})
.collect()
};
let state = if held {
"limited"
} else if !groups.is_empty() {
"partial"
} else if avail.status == crate::route_availability::RouteStatus::Degraded {
"degraded"
} else {
"ok"
};
let info = held
.then(|| crate::rate_limit::get_rate_limit_info(rlk, custom_name))
.flatten();
QuotaJson {
state: state.to_string(),
recovery_at: info.as_ref().and_then(|value| value.recovery_at.clone()),
message: info.as_ref().and_then(|value| value.message.clone()),
source: quota.source,
groups,
used_percent: quota.used_percent,
resets_at: quota.resets_at,
window,
stale: quota.stale,
}
}
pub(crate) fn builtin_profile(name: &str) -> Option<AgentKind> {
AgentKind::ALL_BUILTIN
.iter()
.copied()
.find(|kind| kind.as_str().eq_ignore_ascii_case(name))
}
pub(crate) fn custom_has_endpoint(config: &CustomAgentConfig) -> bool {
config
.base_url
.as_deref()
.map(str::trim)
.is_some_and(|url| !url.is_empty())
}
pub(crate) fn rate_limit_kind(kind: AgentKind, _custom_config: Option<&CustomAgentConfig>) -> AgentKind {
kind
}
pub(crate) fn catalog_default_model(kind: AgentKind) -> Option<String> {
let models = crate::model_catalog::models_for_agent(&kind);
models
.iter()
.find(|model| model.description.to_ascii_lowercase().contains("default"))
.or_else(|| models.first())
.map(|model| model.model.to_string())
}
pub(crate) fn metering_label(shape: crate::types::MeteringShape) -> String {
use crate::types::MeteringShape as M;
match shape {
M::AccountPool => "account_pool",
M::PerModelFamily => "per_model_family",
M::SpendBudget => "spend_budget",
M::Subscription => "subscription",
M::None => "none",
M::Unknown => "unknown",
}
.to_string()
}
#[cfg(test)]
#[path = "agent_json_helpers_tests.rs"]
mod quota_probe_tests;