1use std::collections::HashMap;
7
8#[derive(Debug, Clone, Default)]
10pub struct AgentConfig {
11 pub model: String,
13 pub variant: Option<String>,
15 pub temperature: Option<f64>,
17 pub reasoning_effort: Option<String>,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub enum Harness {
24 OpenCode,
26 ClaudeCode,
28 Codex,
30 GitHubCopilot,
32}
33
34impl Harness {
35 pub fn dir_name(&self) -> &'static str {
37 match self {
38 Self::OpenCode => "opencode",
39 Self::ClaudeCode => "claude-code",
40 Self::Codex => "codex",
41 Self::GitHubCopilot => "github-copilot",
42 }
43 }
44
45 pub fn project_agent_path(&self) -> &'static str {
47 match self {
48 Self::OpenCode => ".opencode/agent",
49 Self::ClaudeCode => ".claude/agents",
50 Self::Codex => ".agents/skills",
51 Self::GitHubCopilot => ".github/agents",
52 }
53 }
54
55 pub fn all() -> &'static [Harness] {
57 &[
58 Self::OpenCode,
59 Self::ClaudeCode,
60 Self::Codex,
61 Self::GitHubCopilot,
62 ]
63 }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
68pub enum AgentTier {
69 Quick,
71 General,
73 Thinking,
75}
76
77impl AgentTier {
78 pub fn file_name(&self) -> &'static str {
80 match self {
81 Self::Quick => "ito-quick",
82 Self::General => "ito-general",
83 Self::Thinking => "ito-thinking",
84 }
85 }
86
87 pub fn all() -> &'static [AgentTier] {
89 &[Self::Quick, Self::General, Self::Thinking]
90 }
91}
92
93pub fn default_agent_configs() -> HashMap<(Harness, AgentTier), AgentConfig> {
95 let mut configs = HashMap::new();
96
97 configs.insert(
99 (Harness::OpenCode, AgentTier::Quick),
100 AgentConfig {
101 model: "anthropic/claude-haiku-4-5".to_string(),
102 temperature: Some(0.3),
103 ..Default::default()
104 },
105 );
106 configs.insert(
107 (Harness::OpenCode, AgentTier::General),
108 AgentConfig {
109 model: "openai/gpt-5.2-codex".to_string(),
110 variant: Some("high".to_string()),
111 temperature: Some(0.3),
112 ..Default::default()
113 },
114 );
115 configs.insert(
116 (Harness::OpenCode, AgentTier::Thinking),
117 AgentConfig {
118 model: "openai/gpt-5.2-codex".to_string(),
119 variant: Some("xhigh".to_string()),
120 temperature: Some(0.5),
121 ..Default::default()
122 },
123 );
124
125 configs.insert(
127 (Harness::ClaudeCode, AgentTier::Quick),
128 AgentConfig {
129 model: "haiku".to_string(),
130 ..Default::default()
131 },
132 );
133 configs.insert(
134 (Harness::ClaudeCode, AgentTier::General),
135 AgentConfig {
136 model: "sonnet".to_string(),
137 ..Default::default()
138 },
139 );
140 configs.insert(
141 (Harness::ClaudeCode, AgentTier::Thinking),
142 AgentConfig {
143 model: "opus".to_string(),
144 ..Default::default()
145 },
146 );
147
148 configs.insert(
150 (Harness::Codex, AgentTier::Quick),
151 AgentConfig {
152 model: "openai/gpt-5.1-codex-mini".to_string(),
153 ..Default::default()
154 },
155 );
156 configs.insert(
157 (Harness::Codex, AgentTier::General),
158 AgentConfig {
159 model: "openai/gpt-5.2-codex".to_string(),
160 reasoning_effort: Some("high".to_string()),
161 ..Default::default()
162 },
163 );
164 configs.insert(
165 (Harness::Codex, AgentTier::Thinking),
166 AgentConfig {
167 model: "openai/gpt-5.2-codex".to_string(),
168 reasoning_effort: Some("xhigh".to_string()),
169 ..Default::default()
170 },
171 );
172
173 configs.insert(
175 (Harness::GitHubCopilot, AgentTier::Quick),
176 AgentConfig {
177 model: "github-copilot/claude-haiku-4.5".to_string(),
178 ..Default::default()
179 },
180 );
181 configs.insert(
182 (Harness::GitHubCopilot, AgentTier::General),
183 AgentConfig {
184 model: "github-copilot/gpt-5.2-codex".to_string(),
185 ..Default::default()
186 },
187 );
188 configs.insert(
189 (Harness::GitHubCopilot, AgentTier::Thinking),
190 AgentConfig {
191 model: "github-copilot/gpt-5.2-codex".to_string(),
192 ..Default::default()
193 },
194 );
195
196 configs
197}
198
199pub fn render_agent_template(template: &str, config: &AgentConfig) -> String {
201 let mut result = template.to_string();
202
203 result = result.replace("{{model}}", &config.model);
205
206 if let Some(variant) = &config.variant {
208 result = result.replace("{{variant}}", variant);
209 } else {
210 result = result
212 .lines()
213 .filter(|line| !line.contains("{{variant}}"))
214 .collect::<Vec<_>>()
215 .join("\n");
216 }
217
218 result
219}
220
221pub fn get_agent_files(harness: Harness) -> Vec<(&'static str, &'static [u8])> {
223 let dir_name = harness.dir_name();
224 let agents_dir = &super::AGENTS_DIR;
225
226 let mut files = Vec::new();
227
228 if let Some(harness_dir) = agents_dir.get_dir(dir_name) {
229 for file in harness_dir.files() {
230 if let Some(name) = file.path().file_name().and_then(|n| n.to_str()) {
231 files.push((name, file.contents()));
232 }
233 }
234
235 for subdir in harness_dir.dirs() {
237 if let Some(skill_file) = subdir.get_file("SKILL.md") {
238 let dir_name = subdir.path().file_name().and_then(|n| n.to_str());
239 if let Some(name) = dir_name {
240 let path = format!("{}/SKILL.md", name);
242 let leaked: &'static str = Box::leak(path.into_boxed_str());
245 files.push((leaked, skill_file.contents()));
246 }
247 }
248 }
249 }
250
251 files
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn render_template_replaces_model() {
260 let template = r#"---
261model: "{{model}}"
262---
263Instructions"#;
264
265 let config = AgentConfig {
266 model: "anthropic/claude-haiku-4-5".to_string(),
267 ..Default::default()
268 };
269
270 let result = render_agent_template(template, &config);
271 assert!(result.contains("model: \"anthropic/claude-haiku-4-5\""));
272 }
273
274 #[test]
275 fn render_template_replaces_variant() {
276 let template = r#"---
277model: "{{model}}"
278variant: "{{variant}}"
279---"#;
280
281 let config = AgentConfig {
282 model: "openai/gpt-5.2-codex".to_string(),
283 variant: Some("high".to_string()),
284 ..Default::default()
285 };
286
287 let result = render_agent_template(template, &config);
288 assert!(result.contains("variant: \"high\""));
289 }
290
291 #[test]
292 fn render_template_removes_variant_line_if_not_set() {
293 let template = r#"---
294model: "{{model}}"
295variant: "{{variant}}"
296---"#;
297
298 let config = AgentConfig {
299 model: "anthropic/claude-haiku-4-5".to_string(),
300 variant: None,
301 ..Default::default()
302 };
303
304 let result = render_agent_template(template, &config);
305 assert!(!result.contains("variant"));
306 }
307
308 #[test]
309 fn default_configs_has_all_combinations() {
310 let configs = default_agent_configs();
311
312 for harness in Harness::all() {
313 for tier in AgentTier::all() {
314 assert!(
315 configs.contains_key(&(*harness, *tier)),
316 "Missing config for {:?}/{:?}",
317 harness,
318 tier
319 );
320 }
321 }
322 }
323}