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::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(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}