Skip to main content

mars_agents/build/
mod.rs

1pub mod bundle;
2pub mod policy;
3pub mod prompt;
4pub mod tool_normalize;
5
6use std::path::PathBuf;
7
8use bundle::{LaunchBundle, ScaffoldSlots, SkillsMetadata, ToolsSpec};
9use policy::{PolicyInput, resolve_policy};
10use prompt::compile_prompt_surface;
11use tool_normalize::{ToolProjectionStatus, is_first_class_harness, normalize_tool_for_harness};
12
13use crate::cli::MarsContext;
14use crate::compiler::agents::{AgentProfile, HarnessKind, parse_agent_content};
15use crate::error::{ConfigError, MarsError};
16
17pub const LAUNCH_BUNDLE_VERSION: u32 = 2;
18
19pub struct LaunchBundleRequest {
20    pub agent: Option<String>,
21    pub model: Option<String>,
22    pub harness: Option<String>,
23    pub effort: Option<String>,
24    pub approval: Option<String>,
25    pub sandbox: Option<String>,
26    pub extra_skills: Vec<String>,
27}
28
29pub fn build_launch_bundle(
30    ctx: &MarsContext,
31    request: LaunchBundleRequest,
32) -> Result<LaunchBundle, MarsError> {
33    let mut warnings: Vec<String> = Vec::new();
34    let profile: AgentProfile;
35    let agent_body: Option<String>;
36
37    if let Some(agent) = request.agent.as_deref() {
38        let agent_path = agent_file_path(&ctx.project_root, agent);
39        let agent_content =
40            std::fs::read_to_string(&agent_path).map_err(|source| MarsError::Io {
41                operation: "read launch bundle agent".to_string(),
42                path: agent_path.clone(),
43                source,
44            })?;
45
46        let mut parse_diags = Vec::new();
47        let (parsed_profile, frontmatter) = parse_agent_content(&agent_content, &mut parse_diags)
48            .map_err(|err| {
49            MarsError::Config(ConfigError::Invalid {
50                message: format!(
51                    "failed to parse agent `{agent}` from {}: {err}",
52                    agent_path.display()
53                ),
54            })
55        })?;
56
57        if let Some(fatal) = parse_diags.iter().find(|diag| diag.is_error()) {
58            return Err(MarsError::Config(ConfigError::Invalid {
59                message: format!(
60                    "agent `{agent}` has invalid frontmatter in {}: {}",
61                    agent_path.display(),
62                    fatal.message()
63                ),
64            }));
65        }
66
67        warnings.extend(
68            parse_diags
69                .iter()
70                .map(|diag| format!("agent `{agent}`: {}", diag.message())),
71        );
72        agent_body = Some(frontmatter.body().to_string());
73        profile = parsed_profile;
74    } else {
75        profile = empty_agent_profile();
76        agent_body = None;
77    }
78
79    let policy = resolve_policy(PolicyInput {
80        project_root: &ctx.project_root,
81        agent: request.agent.as_deref(),
82        profile: &profile,
83        model_override: request.model.as_deref(),
84        config_default_model: None,
85        harness_override: request.harness.as_deref(),
86        effort_override: request.effort.as_deref(),
87        approval_override: request.approval.as_deref(),
88        sandbox_override: request.sandbox.as_deref(),
89    })?;
90
91    warnings.extend(policy.warnings);
92
93    let mars_dir = ctx.project_root.join(".mars");
94    let effective_skills = resolve_effective_skills(&profile, &policy.routing.harness)?;
95
96    let prompt = compile_prompt_surface(
97        &mars_dir,
98        agent_body.as_deref().unwrap_or(""),
99        &effective_skills,
100        &request.extra_skills,
101        &policy.routing.harness,
102        &policy.routing.model_token,
103        &policy.routing.model,
104    )?;
105
106    warnings.extend(prompt.warnings);
107    let (resolved_tools, tool_warnings) = resolve_bundle_tools(&profile, &policy.routing.harness)?;
108    warnings.extend(tool_warnings);
109
110    Ok(LaunchBundle {
111        version: LAUNCH_BUNDLE_VERSION,
112        agent: request.agent,
113        agent_body,
114        routing: policy.routing,
115        execution_policy: policy.execution_policy,
116        prompt_surface: bundle::PromptSurface {
117            system_instruction: prompt.system_instruction,
118            supplemental_documents: prompt.supplemental_documents,
119            inventory_prompt: prompt.inventory_prompt,
120        },
121        scaffold_slots: ScaffoldSlots::placeholders(),
122        tools: resolved_tools,
123        skills_metadata: SkillsMetadata {
124            loaded: prompt.loaded_skills,
125            missing: prompt.missing_skills,
126        },
127        provenance: policy.provenance,
128        warnings,
129    })
130}
131
132fn empty_agent_profile() -> AgentProfile {
133    AgentProfile {
134        name: None,
135        description: None,
136        harness: None,
137        model: None,
138        mode: None,
139        model_invocable: true,
140        approval: None,
141        sandbox: None,
142        effort: None,
143        autocompact: None,
144        autocompact_pct: None,
145        skills: Vec::new(),
146        tools: Vec::new(),
147        tools_denied: Vec::new(),
148        disallowed_tools: Vec::new(),
149        mcp_tools: Vec::new(),
150        harness_overrides: Default::default(),
151        model_policies: Vec::new(),
152        fanout: Vec::new(),
153    }
154}
155
156fn agent_file_path(project_root: &std::path::Path, agent: &str) -> PathBuf {
157    project_root
158        .join(".mars")
159        .join("agents")
160        .join(format!("{agent}.md"))
161}
162
163fn resolve_bundle_tools(
164    profile: &crate::compiler::agents::AgentProfile,
165    harness: &str,
166) -> Result<(ToolsSpec, Vec<String>), MarsError> {
167    let harness_kind = parse_harness_kind(harness)?;
168
169    let effective_tools = profile.effective_tool_policy(&harness_kind);
170    let mut warnings = Vec::new();
171
172    let allowed = normalize_and_dedupe_tools(&effective_tools.allowed, harness, &mut warnings);
173    let disallowed =
174        normalize_and_dedupe_tools(&effective_tools.disallowed, harness, &mut warnings);
175
176    Ok((
177        ToolsSpec {
178            allowed,
179            disallowed,
180            mcp: effective_tools.mcp,
181        },
182        warnings,
183    ))
184}
185
186fn normalize_and_dedupe_tools(
187    tools: &[String],
188    harness: &str,
189    warnings: &mut Vec<String>,
190) -> Vec<String> {
191    let mut seen = std::collections::HashSet::new();
192    let mut projected = Vec::new();
193
194    for tool in tools {
195        let normalized = normalize_tool_for_harness(tool, harness);
196        if normalized.status == ToolProjectionStatus::Unknown && is_first_class_harness(harness) {
197            warnings.push(format!(
198                "tool '{tool}' is not a known {harness} tool; passing through verbatim"
199            ));
200        }
201
202        let trimmed = normalized.name.trim();
203        if trimmed.is_empty() {
204            continue;
205        }
206        if seen.insert(trimmed.to_string()) {
207            projected.push(trimmed.to_string());
208        }
209    }
210
211    projected
212}
213
214fn resolve_effective_skills(
215    profile: &crate::compiler::agents::AgentProfile,
216    harness: &str,
217) -> Result<Vec<String>, MarsError> {
218    let harness_kind = parse_harness_kind(harness)?;
219    Ok(profile.effective_skills(&harness_kind).to_vec())
220}
221
222fn parse_harness_kind(harness: &str) -> Result<HarnessKind, MarsError> {
223    HarnessKind::from_str(harness).ok_or_else(|| {
224        MarsError::Config(ConfigError::Invalid {
225            message: format!(
226                "invalid harness `{harness}` for launch bundle resolution; expected one of: claude, codex, opencode, cursor, pi"
227            ),
228        })
229    })
230}