1use super::InitScope;
2use super::harness::HarnessIntegration;
3use super::recommendations::{ProviderRecommendations, recommended_for_provider};
4use aether_core::agent_spec::ToolFilter;
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 READ_ONLY_DENIED_CODING_TOOLS: &[&str] =
16 &["coding__bash", "coding__edit_file", "coding__lsp_rename", "coding__write_file"];
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 { allow: vec!["coding__bash".to_string(), "skills__*".to_string()], deny: vec![] },
64 ..AgentConfig::default()
65 };
66
67 ResolvedPreset {
68 files: &[TemplateFile { path: SYSTEM_PATH, body: SYSTEM_MD }],
69 settings: AetherSettings { prompts: default_prompts(scope), agents: vec![agent], ..AetherSettings::default() },
70 }
71}
72
73fn batteries_included_preset(
74 provider: Provider,
75 recs: &ProviderRecommendations,
76 scope: InitScope,
77 harnesses: &[HarnessIntegration],
78) -> ResolvedPreset {
79 let display = provider.display_name();
80 let coding = coding_args(harnesses);
81 let skills = skills_args(harnesses);
82
83 let plan = AgentConfig {
84 name: "Plan".to_string(),
85 description: format!("{display} planner (read-only except plan files)"),
86 model: recs.plan.model.to_string(),
87 reasoning_effort: recs.plan.reasoning_effort,
88 user_invocable: true,
89 mcps: vec![mcps(vec![
90 ("plan", vec![]),
91 ("coding", coding.clone()),
92 ("skills", skills.clone()),
93 ("subagents", vec![]),
94 ("tasks", vec![]),
95 ("survey", vec![]),
96 ])],
97 tools: read_only_coding_tools(),
98 ..AgentConfig::default()
99 };
100
101 let build = AgentConfig {
102 name: "Build".to_string(),
103 description: format!("{display} implementor"),
104 model: recs.build.model.to_string(),
105 reasoning_effort: recs.build.reasoning_effort,
106 user_invocable: true,
107 mcps: vec![mcps(vec![
108 ("coding", coding.clone()),
109 ("skills", skills),
110 ("subagents", vec![]),
111 ("tasks", vec![]),
112 ("survey", vec![]),
113 ])],
114 ..AgentConfig::default()
115 };
116
117 let explore = AgentConfig {
118 name: "Explore".to_string(),
119 description: "Explores codebases to find relevant files, patterns, and integration points".to_string(),
120 model: recs.explore.model.to_string(),
121 reasoning_effort: recs.explore.reasoning_effort,
122 agent_invocable: true,
123 prompts: vec![PromptSource::file(scope.asset_path(EXPLORER_AGENTS_PATH))],
124 mcps: vec![mcps(vec![("coding", coding)])],
125 tools: read_only_coding_tools(),
126 ..AgentConfig::default()
127 };
128
129 ResolvedPreset {
130 files: &[
131 TemplateFile { path: SYSTEM_PATH, body: SYSTEM_MD },
132 TemplateFile { path: EXPLORER_AGENTS_PATH, body: EXPLORER_AGENTS_MD },
133 ],
134 settings: AetherSettings {
135 prompts: batteries_prompts(scope, harnesses),
136 agents: vec![plan, build, explore],
137 ..AetherSettings::default()
138 },
139 }
140}
141
142fn skills_args(harnesses: &[HarnessIntegration]) -> Vec<String> {
143 let mut args = vec!["--dir".to_string(), "${AETHER_HOME}/skills".to_string()];
144 for harness in harnesses {
145 for dir in harness.skills_dirs() {
146 args.extend(["--dir".to_string(), dir]);
147 }
148 }
149
150 args.extend([
151 "--dir".to_string(),
152 "${WORKSPACE}/.aether/skills".to_string(),
153 "--notes-dir".to_string(),
154 "${WORKSPACE}/.aether/notes".to_string(),
155 ]);
156
157 args
158}
159
160fn coding_args(harnesses: &[HarnessIntegration]) -> Vec<String> {
161 let mut args = Vec::new();
162 for harness in harnesses {
163 for dir in harness.rules_dirs() {
164 args.extend(["--rules-dir".to_string(), dir]);
165 }
166 }
167 args
168}
169
170fn batteries_prompts(scope: InitScope, harnesses: &[HarnessIntegration]) -> Vec<PromptSource> {
171 let mut prompts = vec![PromptSource::file(scope.asset_path(SYSTEM_PATH))];
172 for harness in harnesses {
173 if let Some(source) = harness.prompt_source() {
174 prompts.push(source);
175 }
176 }
177
178 prompts
179}
180
181fn default_prompts(scope: InitScope) -> Vec<PromptSource> {
182 let mut prompts = vec![PromptSource::file(scope.asset_path(SYSTEM_PATH))];
183 prompts.extend(HarnessIntegration::Agents.prompt_source());
184 prompts
185}
186
187fn mcps(servers: Vec<(&str, Vec<String>)>) -> McpSourceSpec {
188 let servers = servers
189 .into_iter()
190 .map(|(name, args)| {
191 (
192 name.to_string(),
193 McpServerConfig::InMemory(InMemoryServerConfig {
194 type_: InMemoryType::InMemory,
195 args,
196 input: None,
197 proxy: false,
198 }),
199 )
200 })
201 .collect();
202 McpSourceSpec::Inline { servers }
203}
204
205fn read_only_coding_tools() -> ToolFilter {
206 ToolFilter { allow: vec![], deny: READ_ONLY_DENIED_CODING_TOOLS.iter().map(|tool| (*tool).to_string()).collect() }
207}