Skip to main content

aether_cli/init/
build_settings.rs

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    /// Single agent with bash and skills tools only.
22    Minimal,
23    /// Plan + Build + Explore agents wired to the full built-in MCP set.
24    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}