use crate::agent::custom::CustomAgentConfig;
use crate::agent::egress::resolve_agent_egress;
use crate::types::{egress_for_cli, AgentKind};
use std::path::Path;
#[path = "agent_display_quota.rs"]
mod quota;
use quota::{quota_row, QuotaRow, NO_PROBE};
pub(super) fn show_builtin_profile(kind: AgentKind) {
let Some((_, description, cost, best_for, streaming)) = kind.profile() else {
return;
};
println!("Built-in agent: {}", kind.as_str());
println!(" Description: {}", description);
println!(" Cost: {}", cost);
println!(" Best for: {}", best_for);
println!(
" Mode: {}",
if streaming {
"streaming"
} else {
"buffered"
}
);
println!(" Egress: {}", egress_for_cli(kind).label());
}
pub(super) fn print_custom_summary(config: &CustomAgentConfig, path: &Path) {
println!("Custom agent: {}", config.id);
println!(" File: {}", path.display());
println!(" Display name: {}", config.display_name);
println!(" Command: {}", config.command);
println!(" Prompt mode: {}", config.prompt_mode);
println!(" Prompt flag: {}", config.prompt_flag);
println!(" Dir flag: {}", config.dir_flag);
println!(" Model flag: {}", config.model_flag);
println!(" Output flag: {}", config.output_flag);
if config.fixed_args.is_empty() {
println!(" Fixed args: (none)");
} else {
println!(" Fixed args: {}", config.fixed_args.join(" "));
}
println!(" Streaming: {}", config.streaming);
println!(" Output format: {}", config.output_format);
if let Some(url) = config.base_url.as_deref().filter(|s| !s.is_empty()) {
println!(" Base URL: {}", url);
}
println!(" Egress: {}", resolve_agent_egress(&config.id).label());
if !config.strengths.is_empty() {
println!(" Strengths: {}", config.strengths.join(", "));
}
println!(" Capabilities:");
print_capabilities(&config.capabilities);
}
fn print_capabilities(cap: &crate::agent::custom::CapabilityScores) {
for (label, value) in &[
("research", cap.research),
("simple_edit", cap.simple_edit),
("complex_impl", cap.complex_impl),
("frontend", cap.frontend),
("debugging", cap.debugging),
("testing", cap.testing),
("refactoring", cap.refactoring),
("documentation", cap.documentation),
] {
println!(" {:<12} {}", label, value);
}
}
pub(super) fn show_quota() -> anyhow::Result<()> {
use crate::agent::registry;
crate::routing::quota::refresh::refresh_stale_if_enabled();
println!("{:<12} {:<10} DETAIL", "AGENT", "STATUS");
for kind in crate::agent::routable_builtins() {
print_quota_row(kind.as_str(), quota_row(kind, None));
}
for config in registry::list_custom_agents() {
if print_api_quota(&config.id)? { continue; }
print_quota_row(&config.id, quota_row(AgentKind::Custom, Some(config.id.as_str())));
}
Ok(())
}
fn api_quota_line(id: &str) -> anyhow::Result<Option<String>> {
let Some(route) = crate::config::api_route::find(id)? else { return Ok(None) };
let budget = crate::routing::api_budget::for_route(&route, None)?;
let state = budget.billing.label().replace('_', " ").to_uppercase();
Ok(Some(format!("{id:<20} {state:<10} {}", budget.detail())))
}
fn print_api_quota(id: &str) -> anyhow::Result<bool> {
let Some(line) = api_quota_line(id)? else { return Ok(false) };
let row = quota_row(AgentKind::Custom, Some(id));
if matches!(row, QuotaRow::Limited { .. }) { print_quota_row(id, row); }
else { println!("{line}"); }
Ok(true)
}
fn quota_fields(row: QuotaRow) -> (&'static str, String) {
match row {
QuotaRow::Ok { detail } if detail == NO_PROBE => ("UNKNOWN", detail),
QuotaRow::Ok { detail } => ("OK", detail),
QuotaRow::Overage { detail } => ("OVERAGE", detail),
QuotaRow::Exhausted { detail } => ("EXHAUSTED", detail),
QuotaRow::Limited { detail } => ("LIMITED", detail),
QuotaRow::Partial { detail } => ("PARTIAL", detail),
}
}
fn print_quota_row(name: &str, row: QuotaRow) {
let (status, detail) = quota_fields(row);
println!("{name:<12} {status:<10} {detail}");
}
pub(super) fn show_quota_json() -> anyhow::Result<()> {
use serde_json::json;
crate::routing::quota::refresh::refresh_stale_if_enabled();
let mut rows = Vec::new();
let agent_row = |id: &str, row| {
let (status, detail) = quota_fields(row);
json!({"id": id, "kind": "agent", "status": status, "detail": detail})
};
for kind in crate::agent::routable_builtins() {
rows.push(agent_row(kind.as_str(), quota_row(kind, None)));
}
for config in crate::agent::registry::list_custom_agents() {
if let Some(route) = crate::config::api_route::find(&config.id)? {
let budget = crate::routing::api_budget::for_route(&route, None)?;
let hold = match quota_row(AgentKind::Custom, Some(&route.id)) {
QuotaRow::Limited { detail } => Some(detail),
_ => None,
};
rows.push(json!({
"id": route.id, "kind": "api_route", "month": budget.month,
"spend_usd": budget.spent, "budget_usd": budget.budget,
"unpriced_tasks": budget.unpriced, "state": budget.billing.state,
"pace_penalty": budget.pace < 0.0,
"resets_at": budget.resets_at.to_rfc3339(), "hold": hold,
}));
} else {
rows.push(agent_row(&config.id, quota_row(AgentKind::Custom, Some(&config.id))));
}
}
println!("{}", serde_json::to_string(&rows)?);
Ok(())
}
pub(super) fn list_agents() -> anyhow::Result<()> {
use crate::agent::registry;
let rows: Vec<_> = crate::agent::routable_builtins()
.filter_map(|kind| kind.profile().map(|_| (kind, quota_row(kind, None))))
.collect();
let any_hold = rows
.iter()
.any(|(_, row)| !matches!(row, QuotaRow::Ok { .. }));
println!("Built-in agents:");
if any_hold {
println!(" {:<10} {:<12} {:<10} DESCRIPTION", "NAME", "EGRESS", "STATUS");
} else {
println!(" {:<10} {:<12} DESCRIPTION", "NAME", "EGRESS");
}
for (kind, row) in &rows {
let Some((_, description, _, _, _)) = kind.profile() else {
continue;
};
if any_hold {
let status = quota_status_label(*kind, None, row);
println!(
" {:<10} {:<12} {:<10} {}",
kind.as_str(),
egress_for_cli(*kind).label(),
status,
description
);
} else {
println!(
" {:<10} {:<12} {}",
kind.as_str(),
egress_for_cli(*kind).label(),
description
);
}
}
for route in crate::config::load_config()?.api_route { print_api_quota(&route.id)?; }
println!("\nCustom agents:");
let custom: Vec<_> = registry::list_custom_agents().into_iter()
.filter(|config| crate::config::api_route::find(&config.id).ok().flatten().is_none()).collect();
if custom.is_empty() {
println!(" (none installed — use `aid agent add <name>` to create one)");
return Ok(());
}
let custom_rows: Vec<_> = custom
.iter()
.map(|c| (c, quota_row(AgentKind::Custom, Some(c.id.as_str()))))
.collect();
let any_custom_hold = custom_rows
.iter()
.any(|(_, row)| !matches!(row, QuotaRow::Ok { .. }));
if any_custom_hold {
println!(" {:<10} {:<12} {:<10} DISPLAY NAME", "NAME", "EGRESS", "STATUS");
} else {
println!(" {:<10} {:<12} DISPLAY NAME", "NAME", "EGRESS");
}
for (config, row) in &custom_rows {
if any_custom_hold {
let status = quota_status_label(AgentKind::Custom, Some(config.id.as_str()), row);
println!(
" {:<10} {:<12} {:<10} {}",
config.id,
resolve_agent_egress(&config.id).label(),
status,
config.display_name
);
} else {
println!(
" {:<10} {:<12} {}",
config.id,
resolve_agent_egress(&config.id).label(),
config.display_name
);
}
}
Ok(())
}
fn quota_status_label(kind: AgentKind, custom: Option<&str>, row: &QuotaRow) -> String {
match row {
QuotaRow::Ok { .. } => String::new(),
QuotaRow::Overage { .. } => "OVERAGE".to_string(),
QuotaRow::Exhausted { .. } => "EXHAUSTED".to_string(),
QuotaRow::Partial { .. } => "PARTIAL".to_string(),
QuotaRow::Limited { .. } => match crate::routing::rate_limit::get_rate_limit_info(&kind, custom) {
Some(info) if info.needs_human => {
crate::routing::rate_limit::format_hold_end(&kind, custom, &info)
}
_ => "LIMITED".to_string(),
},
}
}
#[cfg(test)]
#[path = "agent_display_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "agent_api_route_tests.rs"]
mod api_route_tests;