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::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(crate) fn build_preset(
42 preset: Preset,
43 provider: Provider,
44 recs: &ProviderRecommendations,
45 scope: InitScope,
46 harnesses: &[HarnessIntegration],
47) -> ResolvedPreset {
48 match preset {
49 Preset::Minimal => minimal_preset(provider, recs, scope),
50 Preset::BatteriesIncluded => batteries_included_preset(provider, recs, scope, harnesses),
51 }
52}
53
54fn minimal_preset(provider: Provider, recs: &ProviderRecommendations, scope: InitScope) -> ResolvedPreset {
55 let display = provider.display_name();
56 let agent = AgentConfig {
57 name: "Default".to_string(),
58 description: format!("{display} A minimal agent with only a bash tool and skills"),
59 model: recs.plan.model.to_string(),
60 reasoning_effort: recs.plan.reasoning_effort,
61 user_invocable: true,
62 mcps: vec![mcps(vec![("coding", vec![]), ("skills", skills_args(&[]))])],
63 tools: ToolFilter {
64 allow: vec![ToolMatcher::name("coding__bash"), ToolMatcher::name("skills__*")],
65 deny: vec![],
66 },
67 ..AgentConfig::default()
68 };
69
70 ResolvedPreset {
71 files: &[TemplateFile { path: SYSTEM_PATH, body: SYSTEM_MD }],
72 settings: AetherSettings { prompts: default_prompts(scope), agents: vec![agent], ..AetherSettings::default() },
73 }
74}
75
76fn batteries_included_preset(
77 provider: Provider,
78 recs: &ProviderRecommendations,
79 scope: InitScope,
80 harnesses: &[HarnessIntegration],
81) -> ResolvedPreset {
82 let display = provider.display_name();
83 let coding = coding_args(harnesses);
84 let skills = skills_args(harnesses);
85
86 let plan = AgentConfig {
87 name: "Plan".to_string(),
88 description: format!("{display} planner (read-only except plan files)"),
89 model: recs.plan.model.to_string(),
90 reasoning_effort: recs.plan.reasoning_effort,
91 user_invocable: true,
92 mcps: vec![mcps(vec![
93 ("plan", vec![]),
94 ("coding", coding.clone()),
95 ("skills", skills.clone()),
96 ("subagents", vec![]),
97 ("tasks", vec![]),
98 ("survey", vec![]),
99 ])],
100 tools: read_only_coding_tools(),
101 ..AgentConfig::default()
102 };
103
104 let build = AgentConfig {
105 name: "Build".to_string(),
106 description: format!("{display} implementor"),
107 model: recs.build.model.to_string(),
108 reasoning_effort: recs.build.reasoning_effort,
109 user_invocable: true,
110 mcps: vec![mcps(vec![
111 ("coding", coding.clone()),
112 ("skills", skills),
113 ("subagents", vec![]),
114 ("tasks", vec![]),
115 ("survey", vec![]),
116 ])],
117 ..AgentConfig::default()
118 };
119
120 let explore = AgentConfig {
121 name: "Explore".to_string(),
122 description: "Explores codebases to find relevant files, patterns, and integration points".to_string(),
123 model: recs.explore.model.to_string(),
124 reasoning_effort: recs.explore.reasoning_effort,
125 agent_invocable: true,
126 prompts: vec![PromptSource::file(scope.asset_path(EXPLORER_AGENTS_PATH))],
127 mcps: vec![mcps(vec![("coding", coding)])],
128 tools: read_only_coding_tools(),
129 ..AgentConfig::default()
130 };
131
132 ResolvedPreset {
133 files: &[
134 TemplateFile { path: SYSTEM_PATH, body: SYSTEM_MD },
135 TemplateFile { path: EXPLORER_AGENTS_PATH, body: EXPLORER_AGENTS_MD },
136 TemplateFile { path: AETHER_SKILL_PATH, body: AETHER_SKILL_MD },
137 ],
138 settings: AetherSettings {
139 prompts: batteries_prompts(scope, harnesses),
140 agents: vec![plan, build, explore],
141 ..AetherSettings::default()
142 },
143 }
144}
145
146fn skills_args(harnesses: &[HarnessIntegration]) -> Vec<String> {
147 let mut args = vec!["--dir".to_string(), "${AETHER_HOME}/skills".to_string()];
148 for harness in harnesses {
149 for dir in harness.skills_dirs() {
150 args.extend(["--dir".to_string(), dir]);
151 }
152 }
153
154 args.extend([
155 "--dir".to_string(),
156 "${WORKSPACE}/.aether/skills".to_string(),
157 "--notes-dir".to_string(),
158 "${WORKSPACE}/.aether/notes".to_string(),
159 ]);
160
161 args
162}
163
164fn coding_args(harnesses: &[HarnessIntegration]) -> Vec<String> {
165 let mut args = Vec::new();
166 for harness in harnesses {
167 for dir in harness.rules_dirs() {
168 args.extend(["--rules-dir".to_string(), dir]);
169 }
170 }
171 args
172}
173
174fn batteries_prompts(scope: InitScope, harnesses: &[HarnessIntegration]) -> Vec<PromptSource> {
175 let mut prompts = vec![PromptSource::file(scope.asset_path(SYSTEM_PATH))];
176 for harness in harnesses {
177 if let Some(source) = harness.prompt_source() {
178 prompts.push(source);
179 }
180 }
181
182 prompts
183}
184
185fn default_prompts(scope: InitScope) -> Vec<PromptSource> {
186 let mut prompts = vec![PromptSource::file(scope.asset_path(SYSTEM_PATH))];
187 prompts.extend(HarnessIntegration::Agents.prompt_source());
188 prompts
189}
190
191fn mcps(servers: Vec<(&str, Vec<String>)>) -> McpSourceSpec {
192 let servers = servers
193 .into_iter()
194 .map(|(name, args)| {
195 (
196 name.to_string(),
197 McpServerConfig::InMemory(InMemoryServerConfig {
198 type_: InMemoryType::InMemory,
199 args,
200 input: None,
201 proxy: false,
202 }),
203 )
204 })
205 .collect();
206 McpSourceSpec::Inline { servers }
207}
208
209fn read_only_coding_tools() -> ToolFilter {
210 ToolFilter {
211 allow: vec![
212 ToolMatcher::read_only(),
213 ToolMatcher::name("plan__*"),
214 ToolMatcher::name("skills__*"),
215 ToolMatcher::name("subagents__*"),
216 ToolMatcher::name("tasks__*"),
217 ToolMatcher::name("survey__*"),
218 ],
219 deny: vec![],
220 }
221}