1use super::InitScope;
2use super::harness::HarnessIntegration;
3use super::recommendations::{ProviderRecommendations, recommended_for_provider};
4use aether_project::{AetherSettings, AgentConfig, McpSourceSpec, PromptSource};
5use llm::{ReasoningEffort, catalog::Provider};
6use mcp_utils::client::{InMemoryServerConfig, InMemoryType, McpServerConfig, ToolExposure};
7use mcp_utils::client::{ToolFilter, ToolMatcher};
8
9const SYSTEM_PATH: &str = "SYSTEM.md";
10const SYSTEM_MD: &str = include_str!("templates/SYSTEM.md");
11
12const EXPLORER_AGENTS_MD: &str = include_str!("templates/agents/codebase-explorer/AGENTS.md");
13const EXPLORER_AGENTS_PATH: &str = "agents/codebase-explorer/AGENTS.md";
14
15const AETHER_SKILL_MD: &str = include_str!("templates/skills/aether/SKILL.md");
16const AETHER_SKILL_PATH: &str = "skills/aether/SKILL.md";
17const PLAN_SKILL_MD: &str = include_str!("templates/skills/plan.md");
18const PLAN_SKILL_PATH: &str = "skills/plan.md";
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
21#[clap(rename_all = "kebab-case")]
22pub enum Preset {
23 Minimal,
25 BatteriesIncluded,
27}
28
29pub(crate) struct ResolvedPreset {
30 pub settings: AetherSettings,
31 pub files: &'static [TemplateFile],
32}
33
34pub(crate) struct TemplateFile {
35 pub path: &'static str,
36 pub body: &'static str,
37}
38
39pub fn supported_providers() -> impl Iterator<Item = Provider> {
40 Provider::ALL.iter().copied().filter(|p| recommended_for_provider(*p).is_some())
41}
42
43pub fn build_batteries_included_settings(
44 display: &str,
45 model: String,
46 reasoning_effort: Option<ReasoningEffort>,
47 scope: InitScope,
48 harnesses: &[HarnessIntegration],
49) -> AetherSettings {
50 let plan = build_batteries_included_plan_agent(display, model.clone(), reasoning_effort, harnesses);
51 let build = build_batteries_included_build_agent(display, model.clone(), reasoning_effort, harnesses);
52 let explore = build_batteries_included_explore_agent(model, reasoning_effort, scope, harnesses);
53
54 AetherSettings {
55 prompts: batteries_prompts(scope, harnesses),
56 agents: vec![plan, build, explore],
57 ..AetherSettings::default()
58 }
59}
60
61fn build_batteries_included_plan_agent(
62 display: &str,
63 model: String,
64 reasoning_effort: Option<ReasoningEffort>,
65 harnesses: &[HarnessIntegration],
66) -> AgentConfig {
67 let coding = coding_args(harnesses);
68 let skills = skills_args(harnesses);
69 AgentConfig {
70 name: "Plan".to_string(),
71 description: format!("{display} planner (may write plan artifacts and request review)"),
72 model,
73 reasoning_effort,
74 user_invocable: true,
75 mcps: vec![mcps(vec![
76 ("review", vec![]),
77 ("coding", coding),
78 ("skills", skills),
79 ("subagents", vec![]),
80 ("tasks", vec![]),
81 ])],
82 tools: plan_tools(),
83 ..AgentConfig::default()
84 }
85}
86
87fn build_batteries_included_build_agent(
88 display: &str,
89 model: String,
90 reasoning_effort: Option<ReasoningEffort>,
91 harnesses: &[HarnessIntegration],
92) -> AgentConfig {
93 let coding = coding_args(harnesses);
94 let skills = skills_args(harnesses);
95 AgentConfig {
96 name: "Build".to_string(),
97 description: format!("{display} implementor"),
98 model,
99 reasoning_effort,
100 user_invocable: true,
101 mcps: vec![mcps(vec![
102 ("coding", coding),
103 ("skills", skills),
104 ("subagents", vec![]),
105 ("tasks", vec![]),
106 ("review", vec![]),
107 ])],
108 ..AgentConfig::default()
109 }
110}
111
112fn build_batteries_included_explore_agent(
113 model: String,
114 reasoning_effort: Option<ReasoningEffort>,
115 scope: InitScope,
116 harnesses: &[HarnessIntegration],
117) -> AgentConfig {
118 let coding = coding_args(harnesses);
119 AgentConfig {
120 name: "Explore".to_string(),
121 description: "Explores codebases to find relevant files, patterns, and integration points".to_string(),
122 model,
123 reasoning_effort,
124 agent_invocable: true,
125 prompts: vec![PromptSource::file(scope.asset_path(EXPLORER_AGENTS_PATH))],
126 mcps: vec![mcps(vec![("coding", coding)])],
127 tools: read_only_coding_tools(),
128 ..AgentConfig::default()
129 }
130}
131
132pub(crate) fn build_preset(
133 preset: Preset,
134 provider: Provider,
135 recs: &ProviderRecommendations,
136 scope: InitScope,
137 harnesses: &[HarnessIntegration],
138) -> ResolvedPreset {
139 match preset {
140 Preset::Minimal => minimal_preset(provider, recs, scope),
141 Preset::BatteriesIncluded => batteries_included_preset(provider, recs, scope, harnesses),
142 }
143}
144
145fn minimal_preset(provider: Provider, recs: &ProviderRecommendations, scope: InitScope) -> ResolvedPreset {
146 let display = provider.display_name();
147 let agent = AgentConfig {
148 name: "Default".to_string(),
149 description: format!("{display} A minimal agent with only a bash tool and skills"),
150 model: recs.plan.model.to_string(),
151 reasoning_effort: recs.plan.reasoning_effort,
152 user_invocable: true,
153 mcps: vec![mcps(vec![("coding", vec![]), ("skills", skills_args(&[]))])],
154 tools: ToolFilter {
155 allow: vec![ToolMatcher::name("coding__bash"), ToolMatcher::name("skills__*")],
156 deny: vec![],
157 },
158 ..AgentConfig::default()
159 };
160
161 ResolvedPreset {
162 files: &[TemplateFile { path: SYSTEM_PATH, body: SYSTEM_MD }],
163 settings: AetherSettings { prompts: default_prompts(scope), agents: vec![agent], ..AetherSettings::default() },
164 }
165}
166
167fn batteries_included_preset(
168 provider: Provider,
169 recs: &ProviderRecommendations,
170 scope: InitScope,
171 harnesses: &[HarnessIntegration],
172) -> ResolvedPreset {
173 let display = provider.display_name();
174 let plan = build_batteries_included_plan_agent(
175 display,
176 recs.plan.model.to_string(),
177 recs.plan.reasoning_effort,
178 harnesses,
179 );
180
181 let build = build_batteries_included_build_agent(
182 display,
183 recs.build.model.to_string(),
184 recs.build.reasoning_effort,
185 harnesses,
186 );
187
188 let explore = build_batteries_included_explore_agent(
189 recs.explore.model.to_string(),
190 recs.explore.reasoning_effort,
191 scope,
192 harnesses,
193 );
194
195 ResolvedPreset {
196 files: &[
197 TemplateFile { path: SYSTEM_PATH, body: SYSTEM_MD },
198 TemplateFile { path: EXPLORER_AGENTS_PATH, body: EXPLORER_AGENTS_MD },
199 TemplateFile { path: AETHER_SKILL_PATH, body: AETHER_SKILL_MD },
200 TemplateFile { path: PLAN_SKILL_PATH, body: PLAN_SKILL_MD },
201 ],
202 settings: AetherSettings {
203 prompts: batteries_prompts(scope, harnesses),
204 agents: vec![plan, build, explore],
205 ..AetherSettings::default()
206 },
207 }
208}
209
210fn skills_args(harnesses: &[HarnessIntegration]) -> Vec<String> {
211 let mut args = vec!["--dir".to_string(), "${AETHER_HOME}/skills".to_string()];
212 for harness in harnesses {
213 for dir in harness.skills_dirs() {
214 args.extend(["--dir".to_string(), dir]);
215 }
216 }
217
218 args.extend(["--dir".to_string(), "${WORKSPACE}/.aether/skills".to_string()]);
219
220 args
221}
222
223fn coding_args(harnesses: &[HarnessIntegration]) -> Vec<String> {
224 let mut args = Vec::new();
225 for harness in harnesses {
226 for dir in harness.rules_dirs() {
227 args.extend(["--rules-dir".to_string(), dir]);
228 }
229 }
230 args
231}
232
233fn batteries_prompts(scope: InitScope, harnesses: &[HarnessIntegration]) -> Vec<PromptSource> {
234 let mut prompts = vec![PromptSource::file(scope.asset_path(SYSTEM_PATH))];
235 for harness in harnesses {
236 if let Some(source) = harness.prompt_source() {
237 prompts.push(source);
238 }
239 }
240
241 prompts
242}
243
244fn default_prompts(scope: InitScope) -> Vec<PromptSource> {
245 let mut prompts = vec![PromptSource::file(scope.asset_path(SYSTEM_PATH))];
246 prompts.extend(HarnessIntegration::Agents.prompt_source());
247 prompts
248}
249
250fn mcps(servers: Vec<(&str, Vec<String>)>) -> McpSourceSpec {
251 let servers = servers
252 .into_iter()
253 .map(|(name, args)| {
254 (
255 name.to_string(),
256 McpServerConfig::InMemory(InMemoryServerConfig {
257 type_: InMemoryType::InMemory,
258 args,
259 input: None,
260 defer_tools: ToolExposure::ModelVisible,
261 }),
262 )
263 })
264 .collect();
265 McpSourceSpec::Inline { servers }
266}
267
268fn plan_tools() -> ToolFilter {
269 let mut tools = read_only_coding_tools();
270 tools.allow.extend([
271 ToolMatcher::name("coding__write_file"),
272 ToolMatcher::name("coding__edit_file"),
273 ToolMatcher::name("review__review_artifact"),
274 ]);
275 tools
276}
277
278fn read_only_coding_tools() -> ToolFilter {
279 ToolFilter {
280 allow: vec![
281 ToolMatcher::read_only(),
282 ToolMatcher::name("skills__*"),
283 ToolMatcher::name("subagents__*"),
284 ToolMatcher::name("tasks__*"),
285 ],
286 deny: vec![],
287 }
288}