Skip to main content

aether_cli/init/
build_settings.rs

1use super::InitScope;
2use super::recommendations::{ProviderRecommendations, recommended_for_provider};
3use aether_core::agent_spec::ToolFilter;
4use aether_project::{AetherSettings, AgentConfig, McpSourceSpec, PromptSource};
5use llm::catalog::Provider;
6use mcp_utils::client::{InMemoryServerConfig, InMemoryType, McpServerConfig};
7
8const SYSTEM_PATH: &str = "SYSTEM.md";
9const PROJECT_AGENTS_PATH: &str = "${WORKSPACE}/AGENTS.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 SKILLS_ARGS: &[&str] = &[
16    "--dir",
17    "${AETHER_HOME}/skills",
18    "--dir",
19    "${WORKSPACE}/.aether/skills",
20    "--notes-dir",
21    "${WORKSPACE}/.aether/notes",
22];
23
24const READ_ONLY_DENIED_CODING_TOOLS: &[&str] =
25    &["coding__bash", "coding__edit_file", "coding__lsp_rename", "coding__write_file"];
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)]
28#[clap(rename_all = "kebab-case")]
29pub enum Preset {
30    /// Single agent with bash and skills tools only.
31    Minimal,
32    /// Plan + Build + Explore agents wired to the full built-in MCP set.
33    BatteriesIncluded,
34}
35
36pub(crate) struct ResolvedPreset {
37    pub settings: AetherSettings,
38    pub files: &'static [TemplateFile],
39}
40
41pub(crate) struct TemplateFile {
42    pub path: &'static str,
43    pub body: &'static str,
44}
45
46pub fn supported_providers() -> impl Iterator<Item = Provider> {
47    Provider::ALL.iter().copied().filter(|p| recommended_for_provider(*p).is_some())
48}
49
50pub(crate) fn build_preset(
51    preset: Preset,
52    provider: Provider,
53    recs: &ProviderRecommendations,
54    scope: InitScope,
55) -> ResolvedPreset {
56    match preset {
57        Preset::Minimal => minimal_preset(provider, recs, scope),
58        Preset::BatteriesIncluded => build_batteries_included_preset(provider, recs, scope),
59    }
60}
61
62fn minimal_preset(provider: Provider, recs: &ProviderRecommendations, scope: InitScope) -> ResolvedPreset {
63    let display = provider.display_name();
64    let agent = AgentConfig {
65        name: "Default".to_string(),
66        description: format!("{display} A minimal agent with only a bash tool and skills"),
67        model: recs.plan.model.to_string(),
68        reasoning_effort: recs.plan.reasoning_effort,
69        user_invocable: true,
70        mcps: vec![mcps(&[("coding", &[]), ("skills", SKILLS_ARGS)])],
71        tools: ToolFilter { allow: vec!["coding__bash".to_string(), "skills__*".to_string()], deny: vec![] },
72        ..AgentConfig::default()
73    };
74
75    ResolvedPreset {
76        files: &[TemplateFile { path: SYSTEM_PATH, body: SYSTEM_MD }],
77        settings: AetherSettings { prompts: default_prompts(scope), agents: vec![agent], ..AetherSettings::default() },
78    }
79}
80
81fn build_batteries_included_preset(
82    provider: Provider,
83    recs: &ProviderRecommendations,
84    scope: InitScope,
85) -> ResolvedPreset {
86    let display = provider.display_name();
87    let plan = AgentConfig {
88        name: "Plan".to_string(),
89        description: format!("{display} planner (read-only)"),
90        model: recs.plan.model.to_string(),
91        reasoning_effort: recs.plan.reasoning_effort,
92        user_invocable: true,
93        mcps: vec![mcps(&[
94            ("plan", &[]),
95            ("coding", &[]),
96            ("skills", SKILLS_ARGS),
97            ("subagents", &[]),
98            ("tasks", &[]),
99            ("survey", &[]),
100        ])],
101        tools: read_only_coding_tools(),
102        ..AgentConfig::default()
103    };
104
105    let build = AgentConfig {
106        name: "Build".to_string(),
107        description: format!("{display} implementor"),
108        model: recs.build.model.to_string(),
109        reasoning_effort: recs.build.reasoning_effort,
110        user_invocable: true,
111        mcps: vec![mcps(&[
112            ("coding", &[]),
113            ("skills", SKILLS_ARGS),
114            ("subagents", &[]),
115            ("tasks", &[]),
116            ("survey", &[]),
117        ])],
118        ..AgentConfig::default()
119    };
120
121    let explore = AgentConfig {
122        name: "Explore".to_string(),
123        description: "Explores codebases to find relevant files, patterns, and integration points".to_string(),
124        model: recs.explore.model.to_string(),
125        reasoning_effort: recs.explore.reasoning_effort,
126        agent_invocable: true,
127        prompts: vec![PromptSource::file(settings_asset_path(scope, EXPLORER_AGENTS_PATH))],
128        mcps: vec![mcps(&[("coding", &[])])],
129        tools: read_only_coding_tools(),
130        ..AgentConfig::default()
131    };
132
133    ResolvedPreset {
134        files: &[
135            TemplateFile { path: SYSTEM_PATH, body: SYSTEM_MD },
136            TemplateFile { path: EXPLORER_AGENTS_PATH, body: EXPLORER_AGENTS_MD },
137        ],
138        settings: AetherSettings {
139            prompts: default_prompts(scope),
140            agents: vec![plan, build, explore],
141            ..AetherSettings::default()
142        },
143    }
144}
145
146fn read_only_coding_tools() -> ToolFilter {
147    ToolFilter { allow: vec![], deny: READ_ONLY_DENIED_CODING_TOOLS.iter().map(|tool| (*tool).to_string()).collect() }
148}
149
150fn default_prompts(scope: InitScope) -> Vec<PromptSource> {
151    vec![
152        PromptSource::file(settings_asset_path(scope, SYSTEM_PATH)),
153        PromptSource::file(PROJECT_AGENTS_PATH).optional(),
154    ]
155}
156
157fn settings_asset_path(scope: InitScope, asset_rel_path: &str) -> String {
158    match scope {
159        InitScope::User => asset_rel_path.to_string(),
160        InitScope::Project => format!(".aether/{asset_rel_path}"),
161    }
162}
163
164fn mcps(servers: &[(&str, &[&str])]) -> McpSourceSpec {
165    let servers = servers
166        .iter()
167        .map(|(name, args)| {
168            (
169                (*name).to_string(),
170                McpServerConfig::InMemory(InMemoryServerConfig {
171                    type_: InMemoryType::InMemory,
172                    args: args.iter().map(|s| (*s).to_string()).collect(),
173                    input: None,
174                    proxy: false,
175                }),
176            )
177        })
178        .collect();
179    McpSourceSpec::Inline { servers }
180}