Files
dev/code-rs/core/src/agent_defaults.rs
T

417 lines
14 KiB
Rust

//! Defaults for agent model slugs and their CLI launch configuration.
//!
//! The canonical catalog defined here is consumed by both the core executor
//! (to assemble argv when the user has not overridden a model) and by the TUI
//! (to surface the available sub-agent options).
use crate::config_types::AgentConfig;
use std::collections::{HashMap, HashSet};
const CLAUDE_ALLOWED_TOOLS: &str = "Bash(ls:*), Bash(cat:*), Bash(grep:*), Bash(git status:*), Bash(git log:*), Bash(find:*), Read, Grep, Glob, LS, WebFetch, TodoRead, TodoWrite, WebSearch";
const CLOUD_MODEL_ENV_FLAG: &str = "CODE_ENABLE_CLOUD_AGENT_MODEL";
const CODE_GPT5_CODEX_READ_ONLY: &[&str] = &["-s", "read-only", "-a", "never", "exec", "--skip-git-repo-check"];
const CODE_GPT5_CODEX_WRITE: &[&str] = &["-s", "workspace-write", "--dangerously-bypass-approvals-and-sandbox", "exec", "--skip-git-repo-check"];
const CODE_GPT5_READ_ONLY: &[&str] = &["-s", "read-only", "-a", "never", "exec", "--skip-git-repo-check"];
const CODE_GPT5_WRITE: &[&str] = &["-s", "workspace-write", "--dangerously-bypass-approvals-and-sandbox", "exec", "--skip-git-repo-check"];
const CLAUDE_SONNET_READ_ONLY: &[&str] = &["--allowedTools", CLAUDE_ALLOWED_TOOLS];
const CLAUDE_SONNET_WRITE: &[&str] = &["--dangerously-skip-permissions"];
const CLAUDE_OPUS_READ_ONLY: &[&str] = &["--allowedTools", CLAUDE_ALLOWED_TOOLS];
const CLAUDE_OPUS_WRITE: &[&str] = &["--dangerously-skip-permissions"];
const CLAUDE_HAIKU_READ_ONLY: &[&str] = &["--allowedTools", CLAUDE_ALLOWED_TOOLS];
const CLAUDE_HAIKU_WRITE: &[&str] = &["--dangerously-skip-permissions"];
const GEMINI_PRO_READ_ONLY: &[&str] = &[];
const GEMINI_PRO_WRITE: &[&str] = &["-y"];
const GEMINI_FLASH_READ_ONLY: &[&str] = &[];
const GEMINI_FLASH_WRITE: &[&str] = &["-y"];
const QWEN_3_CODER_READ_ONLY: &[&str] = &[];
const QWEN_3_CODER_WRITE: &[&str] = &["-y"];
const CLOUD_GPT5_CODEX_READ_ONLY: &[&str] = &[];
const CLOUD_GPT5_CODEX_WRITE: &[&str] = &[];
/// Canonical list of built-in agent model slugs used when no `[[agents]]`
/// entries are configured. The ordering here controls priority for legacy
/// CLI-name lookups.
pub const DEFAULT_AGENT_NAMES: &[&str] = &[
// Frontline for moderate/challenging tasks
"code-gpt-5.1-codex-max",
"claude-opus-4.5",
"gemini-3-pro",
// Straightforward / cost-aware
"code-gpt-5.1-codex-mini",
"claude-sonnet-4.5",
"gemini-2.5-flash",
// Mixed/general and alternates
"code-gpt-5.1",
"claude-haiku-4.5",
"qwen-3-coder",
"cloud-gpt-5.1-codex-max",
];
#[derive(Debug, Clone)]
pub struct AgentModelSpec {
pub slug: &'static str,
pub family: &'static str,
pub cli: &'static str,
pub read_only_args: &'static [&'static str],
pub write_args: &'static [&'static str],
pub model_args: &'static [&'static str],
pub description: &'static str,
pub enabled_by_default: bool,
pub aliases: &'static [&'static str],
pub gating_env: Option<&'static str>,
pub is_frontline: bool,
}
impl AgentModelSpec {
pub fn is_enabled(&self) -> bool {
if self.enabled_by_default {
return true;
}
if let Some(env) = self.gating_env {
if let Ok(value) = std::env::var(env) {
return matches!(value.as_str(), "1" | "true" | "TRUE" | "True");
}
}
false
}
pub fn default_args(&self, read_only: bool) -> &'static [&'static str] {
if read_only {
self.read_only_args
} else {
self.write_args
}
}
}
const AGENT_MODEL_SPECS: &[AgentModelSpec] = &[
AgentModelSpec {
slug: "code-gpt-5.1-codex-max",
family: "code",
cli: "coder",
read_only_args: CODE_GPT5_CODEX_READ_ONLY,
write_args: CODE_GPT5_CODEX_WRITE,
model_args: &["--model", "gpt-5.1-codex-max"],
description: "Frontline coding agent for all work; top of the line speed, reasoning and execution.",
enabled_by_default: true,
aliases: &["code-gpt-5.1-codex", "code-gpt-5-codex", "coder", "code", "codex"],
gating_env: None,
is_frontline: true,
},
AgentModelSpec {
slug: "code-gpt-5.1-codex-mini",
family: "code",
cli: "coder",
read_only_args: CODE_GPT5_CODEX_READ_ONLY,
write_args: CODE_GPT5_CODEX_WRITE,
model_args: &["--model", "gpt-5.1-codex-mini"],
description: "Straightforward coding tasks: cheapest and quick; great for implementation, refactors, multi-file edits, and code reviews.",
enabled_by_default: true,
aliases: &["code-gpt-5-codex-mini", "codex-mini", "coder-mini"],
gating_env: None,
is_frontline: false,
},
AgentModelSpec {
slug: "code-gpt-5.1",
family: "code",
cli: "coder",
read_only_args: CODE_GPT5_READ_ONLY,
write_args: CODE_GPT5_WRITE,
model_args: &["--model", "gpt-5.1"],
description: "Mixed tasks that blend code with design/product reasoning; slower speed, but larger breadth.",
enabled_by_default: true,
aliases: &["code-gpt-5", "coder-gpt-5"],
gating_env: None,
is_frontline: false,
},
AgentModelSpec {
slug: "claude-opus-4.5",
family: "claude",
cli: "claude",
read_only_args: CLAUDE_OPUS_READ_ONLY,
write_args: CLAUDE_OPUS_WRITE,
model_args: &["--model", "opus"],
description: "Frontline Claude for challenging or high-stakes tasks; excels at all coding tasks and novel problem solving.",
enabled_by_default: true,
aliases: &["claude-opus", "claude-opus-4.1"],
gating_env: None,
is_frontline: true,
},
AgentModelSpec {
slug: "claude-sonnet-4.5",
family: "claude",
cli: "claude",
read_only_args: CLAUDE_SONNET_READ_ONLY,
write_args: CLAUDE_SONNET_WRITE,
model_args: &["--model", "sonnet"],
description: "Straightforward coding/support tasks; strong at implementation, tool use, debugging, and testing.",
enabled_by_default: true,
aliases: &["claude", "claude-sonnet"],
gating_env: None,
is_frontline: false,
},
AgentModelSpec {
slug: "claude-haiku-4.5",
family: "claude",
cli: "claude",
read_only_args: CLAUDE_HAIKU_READ_ONLY,
write_args: CLAUDE_HAIKU_WRITE,
model_args: &["--model", "haiku"],
description: "Very fast model for simple tasks. Similar to gemini-2.5-flash in capability.",
enabled_by_default: true,
aliases: &["claude-haiku"],
gating_env: None,
is_frontline: false,
},
AgentModelSpec {
slug: "gemini-3-pro",
family: "gemini",
cli: "gemini",
read_only_args: GEMINI_PRO_READ_ONLY,
write_args: GEMINI_PRO_WRITE,
model_args: &["--model", "pro"],
description: "Frontline Gemini for challenging work; strong multimodal and high level reasoning.",
enabled_by_default: true,
aliases: &["gemini-3-pro-preview", "gemini-3", "gemini3", "gemini", "gemini-2.5-pro"],
gating_env: None,
is_frontline: true,
},
AgentModelSpec {
slug: "gemini-2.5-flash",
family: "gemini",
cli: "gemini",
read_only_args: GEMINI_FLASH_READ_ONLY,
write_args: GEMINI_FLASH_WRITE,
model_args: &["--model", "flash"],
description: "Straightforward / budget tasks: very fast for scaffolding, minimal repros/tests, or high-volume edits.",
enabled_by_default: true,
aliases: &["gemini-flash"],
gating_env: None,
is_frontline: false,
},
AgentModelSpec {
slug: "qwen-3-coder",
family: "qwen",
cli: "qwen",
read_only_args: QWEN_3_CODER_READ_ONLY,
write_args: QWEN_3_CODER_WRITE,
model_args: &["-m", "qwen-3-coder"],
description: "Fast and reasonably effective. Good for providing an alternative opinion as it has quite different training data to other models.",
enabled_by_default: true,
aliases: &["qwen", "qwen3"],
gating_env: None,
is_frontline: false,
},
AgentModelSpec {
slug: "cloud-gpt-5.1-codex-max",
family: "cloud",
cli: "cloud",
read_only_args: CLOUD_GPT5_CODEX_READ_ONLY,
write_args: CLOUD_GPT5_CODEX_WRITE,
model_args: &["--model", "gpt-5.1-codex-max"],
description: "Cloud-hosted gpt-5.1-codex-max agent. Requires the CODE_ENABLE_CLOUD_AGENT_MODEL flag and carries the latency of a remote run.",
enabled_by_default: false,
aliases: &["cloud-gpt-5.1-codex", "cloud-gpt-5-codex", "cloud"],
gating_env: Some(CLOUD_MODEL_ENV_FLAG),
is_frontline: false,
},
];
pub fn agent_model_specs() -> &'static [AgentModelSpec] {
AGENT_MODEL_SPECS
}
pub fn enabled_agent_model_specs() -> Vec<&'static AgentModelSpec> {
AGENT_MODEL_SPECS
.iter()
.filter(|spec| spec.is_enabled())
.collect()
}
pub fn agent_model_spec(identifier: &str) -> Option<&'static AgentModelSpec> {
let lower = identifier.to_ascii_lowercase();
AGENT_MODEL_SPECS
.iter()
.find(|spec| spec.slug.eq_ignore_ascii_case(&lower))
.or_else(|| {
AGENT_MODEL_SPECS.iter().find(|spec| {
spec.aliases
.iter()
.any(|alias| alias.eq_ignore_ascii_case(&lower))
})
})
}
fn model_guide_intro(active_agents: &[String]) -> String {
let mut present_frontline: Vec<String> = active_agents
.iter()
.filter_map(|id| {
agent_model_spec(id)
.filter(|spec| spec.is_frontline)
.map(|spec| spec.slug.to_string())
})
.collect();
if present_frontline.is_empty() {
present_frontline.push("code-gpt-5.1-codex-max".to_string());
}
let frontline_str = present_frontline.join(", ");
format!(
"Preferred agent models: use {frontline} for challenging coding/agentic work.",
frontline = frontline_str,
)
}
fn model_guide_line(spec: &AgentModelSpec) -> String {
format!("- `{}`: {}", spec.slug, spec.description)
}
fn custom_model_guide_line(name: &str, description: &str) -> String {
format!("- `{}`: {}", name, description)
}
pub fn build_model_guide_description(active_agents: &[String]) -> String {
let mut description = model_guide_intro(active_agents);
let mut canonical: HashSet<String> = HashSet::new();
for name in active_agents {
let trimmed = name.trim();
if trimmed.is_empty() {
continue;
}
if let Some(spec) = agent_model_spec(trimmed) {
canonical.insert(spec.slug.to_ascii_lowercase());
} else {
canonical.insert(trimmed.to_ascii_lowercase());
}
}
let lines: Vec<String> = AGENT_MODEL_SPECS
.iter()
.filter(|spec| canonical.contains(&spec.slug.to_ascii_lowercase()))
.map(model_guide_line)
.collect();
if lines.is_empty() {
description.push('\n');
description.push_str("- No model guides available for the current configuration.");
} else {
for line in lines {
description.push('\n');
description.push_str(&line);
}
}
description
}
pub fn model_guide_markdown() -> String {
AGENT_MODEL_SPECS
.iter()
.filter(|spec| spec.is_enabled())
.map(model_guide_line)
.collect::<Vec<_>>()
.join("\n")
}
pub fn model_guide_markdown_with_custom(configured_agents: &[AgentConfig]) -> Option<String> {
let mut lines: Vec<String> = Vec::new();
let mut positions: HashMap<String, usize> = HashMap::new();
for spec in AGENT_MODEL_SPECS.iter().filter(|spec| spec.is_enabled()) {
let idx = lines.len();
positions.insert(spec.slug.to_ascii_lowercase(), idx);
lines.push(model_guide_line(spec));
}
let mut saw_custom = false;
for agent in configured_agents {
if !agent.enabled {
continue;
}
let Some(description) = agent.description.as_deref() else { continue };
let trimmed = description.trim();
if trimmed.is_empty() {
continue;
}
let slug = agent.name.trim();
if slug.is_empty() {
continue;
}
saw_custom = true;
let line = custom_model_guide_line(slug, trimmed);
let key = slug.to_ascii_lowercase();
if let Some(idx) = positions.get(&key).copied() {
lines[idx] = line;
} else {
positions.insert(key, lines.len());
lines.push(line);
}
}
if saw_custom {
Some(lines.join("\n"))
} else {
None
}
}
pub fn default_agent_configs() -> Vec<AgentConfig> {
enabled_agent_model_specs()
.into_iter()
.map(|spec| agent_config_from_spec(spec))
.collect()
}
pub fn agent_config_from_spec(spec: &AgentModelSpec) -> AgentConfig {
AgentConfig {
name: spec.slug.to_string(),
command: spec.cli.to_string(),
args: Vec::new(),
read_only: false,
enabled: spec.is_enabled(),
description: None,
env: None,
args_read_only: some_args(spec.read_only_args),
args_write: some_args(spec.write_args),
instructions: None,
}
}
fn some_args(args: &[&str]) -> Option<Vec<String>> {
if args.is_empty() {
None
} else {
Some(args.iter().map(|arg| (*arg).to_string()).collect())
}
}
/// Return default CLI arguments (excluding the prompt flag) for a given agent
/// identifier and access mode.
///
/// The identifier can be either the canonical slug or a legacy CLI alias
/// (`code`, `claude`, etc.) used prior to the model slug transition.
pub fn default_params_for(name: &str, read_only: bool) -> Vec<String> {
if let Some(spec) = agent_model_spec(name) {
return spec
.default_args(read_only)
.iter()
.map(|arg| (*arg).to_string())
.collect();
}
Vec::new()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cloud_defaults_are_empty_both_modes() {
assert!(default_params_for("cloud", true).is_empty());
assert!(default_params_for("cloud", false).is_empty());
}
}