use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct AgentConfig {
pub model: String,
pub variant: Option<String>,
pub temperature: Option<f64>,
pub reasoning_effort: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Harness {
OpenCode,
ClaudeCode,
Codex,
GitHubCopilot,
Pi,
}
impl Harness {
pub fn dir_name(&self) -> &'static str {
match self {
Self::OpenCode => "opencode",
Self::ClaudeCode => "claude-code",
Self::Codex => "codex",
Self::GitHubCopilot => "github-copilot",
Self::Pi => "pi",
}
}
pub fn project_agent_path(&self) -> &'static str {
match self {
Self::OpenCode => ".opencode/agent",
Self::ClaudeCode => ".claude/agents",
Self::Codex => ".agents/skills",
Self::GitHubCopilot => ".github/agents",
Self::Pi => ".pi/agents",
}
}
pub fn all() -> &'static [Harness] {
&[
Self::OpenCode,
Self::ClaudeCode,
Self::Codex,
Self::GitHubCopilot,
Self::Pi,
]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AgentTier {
Quick,
General,
Thinking,
}
impl AgentTier {
pub fn file_name(&self) -> &'static str {
match self {
Self::Quick => "ito-quick",
Self::General => "ito-general",
Self::Thinking => "ito-thinking",
}
}
pub fn all() -> &'static [AgentTier] {
&[Self::Quick, Self::General, Self::Thinking]
}
}
pub fn default_agent_configs() -> HashMap<(Harness, AgentTier), AgentConfig> {
let mut configs = HashMap::new();
configs.insert(
(Harness::OpenCode, AgentTier::Quick),
AgentConfig {
model: "anthropic/claude-haiku-4-5".to_string(),
temperature: Some(0.3),
..Default::default()
},
);
configs.insert(
(Harness::OpenCode, AgentTier::General),
AgentConfig {
model: "openai/gpt-5.4".to_string(),
variant: Some("high".to_string()),
temperature: Some(0.3),
..Default::default()
},
);
configs.insert(
(Harness::OpenCode, AgentTier::Thinking),
AgentConfig {
model: "openai/gpt-5.4".to_string(),
variant: Some("xhigh".to_string()),
temperature: Some(0.5),
..Default::default()
},
);
configs.insert(
(Harness::ClaudeCode, AgentTier::Quick),
AgentConfig {
model: "haiku".to_string(),
..Default::default()
},
);
configs.insert(
(Harness::ClaudeCode, AgentTier::General),
AgentConfig {
model: "sonnet".to_string(),
..Default::default()
},
);
configs.insert(
(Harness::ClaudeCode, AgentTier::Thinking),
AgentConfig {
model: "opus".to_string(),
..Default::default()
},
);
configs.insert(
(Harness::Codex, AgentTier::Quick),
AgentConfig {
model: "openai/gpt-5-mini".to_string(),
..Default::default()
},
);
configs.insert(
(Harness::Codex, AgentTier::General),
AgentConfig {
model: "openai/gpt-5.4".to_string(),
reasoning_effort: Some("high".to_string()),
..Default::default()
},
);
configs.insert(
(Harness::Codex, AgentTier::Thinking),
AgentConfig {
model: "openai/gpt-5.4".to_string(),
reasoning_effort: Some("xhigh".to_string()),
..Default::default()
},
);
configs.insert(
(Harness::Pi, AgentTier::Quick),
AgentConfig {
model: "claude-haiku-4-5".to_string(),
..Default::default()
},
);
configs.insert(
(Harness::Pi, AgentTier::General),
AgentConfig {
model: "claude-sonnet-4-6".to_string(),
..Default::default()
},
);
configs.insert(
(Harness::Pi, AgentTier::Thinking),
AgentConfig {
model: "claude-sonnet-4-6".to_string(),
..Default::default()
},
);
configs.insert(
(Harness::GitHubCopilot, AgentTier::Quick),
AgentConfig {
model: "github-copilot/claude-haiku-4.5".to_string(),
..Default::default()
},
);
configs.insert(
(Harness::GitHubCopilot, AgentTier::General),
AgentConfig {
model: "github-copilot/gpt-5.4".to_string(),
..Default::default()
},
);
configs.insert(
(Harness::GitHubCopilot, AgentTier::Thinking),
AgentConfig {
model: "github-copilot/gpt-5.4".to_string(),
..Default::default()
},
);
configs
}
pub fn render_agent_template(template: &str, config: &AgentConfig) -> String {
let mut result = template.to_string();
result = result.replace("{{model}}", &config.model);
if let Some(variant) = &config.variant {
result = result.replace("{{variant}}", variant);
} else {
result = result
.lines()
.filter(|line| !line.contains("{{variant}}"))
.collect::<Vec<_>>()
.join("\n");
}
result
}
pub fn get_agent_files(harness: Harness) -> Vec<(&'static str, &'static [u8])> {
let dir_name = harness.dir_name();
let agents_dir = &super::AGENTS_DIR;
let mut files = Vec::new();
if let Some(harness_dir) = agents_dir.get_dir(dir_name) {
for file in harness_dir.files() {
if let Some(name) = file.path().file_name().and_then(|n| n.to_str()) {
files.push((name, file.contents()));
}
}
for subdir in harness_dir.dirs() {
if let Some(skill_file) = subdir.get_file("SKILL.md") {
let dir_name = subdir.path().file_name().and_then(|n| n.to_str());
if let Some(name) = dir_name {
let path = format!("{}/SKILL.md", name);
let leaked: &'static str = Box::leak(path.into_boxed_str());
files.push((leaked, skill_file.contents()));
}
}
}
}
files
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_template_replaces_model() {
let template = r#"---
model: "{{model}}"
---
Instructions"#;
let config = AgentConfig {
model: "anthropic/claude-haiku-4-5".to_string(),
..Default::default()
};
let result = render_agent_template(template, &config);
assert!(result.contains("model: \"anthropic/claude-haiku-4-5\""));
}
#[test]
fn render_template_replaces_variant() {
let template = r#"---
model: "{{model}}"
variant: "{{variant}}"
---"#;
let config = AgentConfig {
model: "openai/gpt-5.2-codex".to_string(),
variant: Some("high".to_string()),
..Default::default()
};
let result = render_agent_template(template, &config);
assert!(result.contains("variant: \"high\""));
}
#[test]
fn render_template_removes_variant_line_if_not_set() {
let template = r#"---
model: "{{model}}"
variant: "{{variant}}"
---"#;
let config = AgentConfig {
model: "anthropic/claude-haiku-4-5".to_string(),
variant: None,
..Default::default()
};
let result = render_agent_template(template, &config);
assert!(!result.contains("variant"));
}
#[test]
fn default_configs_has_all_combinations() {
let configs = default_agent_configs();
for harness in Harness::all() {
for tier in AgentTier::all() {
assert!(
configs.contains_key(&(*harness, *tier)),
"Missing config for {:?}/{:?}",
harness,
tier
);
}
}
}
}