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