Skip to main content

mars_agents/build/
mod.rs

1pub mod bundle;
2pub mod policy;
3pub mod prompt;
4
5use std::path::PathBuf;
6
7use bundle::{LaunchBundle, ScaffoldSlots, SkillsMetadata, ToolsSpec};
8use policy::{PolicyInput, resolve_policy};
9use prompt::compile_prompt_surface;
10
11use crate::cli::MarsContext;
12use crate::compiler::agents::{HarnessKind, parse_agent_content};
13use crate::error::{ConfigError, MarsError};
14
15pub struct LaunchBundleRequest {
16    pub agent: String,
17    pub model: Option<String>,
18    pub harness: Option<String>,
19    pub effort: Option<String>,
20    pub approval: Option<String>,
21    pub sandbox: Option<String>,
22    pub extra_skills: Vec<String>,
23}
24
25pub fn build_launch_bundle(
26    ctx: &MarsContext,
27    request: LaunchBundleRequest,
28) -> Result<LaunchBundle, MarsError> {
29    let agent_path = agent_file_path(&ctx.project_root, &request.agent);
30    let agent_content = std::fs::read_to_string(&agent_path).map_err(|source| MarsError::Io {
31        operation: "read launch bundle agent".to_string(),
32        path: agent_path.clone(),
33        source,
34    })?;
35
36    let mut parse_diags = Vec::new();
37    let (profile, frontmatter) =
38        parse_agent_content(&agent_content, &mut parse_diags).map_err(|err| {
39            MarsError::Config(ConfigError::Invalid {
40                message: format!(
41                    "failed to parse agent `{}` from {}: {err}",
42                    request.agent,
43                    agent_path.display()
44                ),
45            })
46        })?;
47
48    if let Some(fatal) = parse_diags.iter().find(|diag| diag.is_error()) {
49        return Err(MarsError::Config(ConfigError::Invalid {
50            message: format!(
51                "agent `{}` has invalid frontmatter in {}: {}",
52                request.agent,
53                agent_path.display(),
54                fatal.message()
55            ),
56        }));
57    }
58
59    let mut warnings: Vec<String> = parse_diags
60        .iter()
61        .map(|diag| format!("agent `{}`: {}", request.agent, diag.message()))
62        .collect();
63
64    let policy = resolve_policy(PolicyInput {
65        project_root: &ctx.project_root,
66        profile: &profile,
67        model_override: request.model.as_deref(),
68        harness_override: request.harness.as_deref(),
69        effort_override: request.effort.as_deref(),
70        approval_override: request.approval.as_deref(),
71        sandbox_override: request.sandbox.as_deref(),
72    })?;
73
74    warnings.extend(policy.warnings);
75
76    let mars_dir = ctx.project_root.join(".mars");
77    let effective_skills = resolve_effective_skills(&profile, &policy.routing.harness)?;
78
79    let prompt = compile_prompt_surface(
80        &mars_dir,
81        frontmatter.body(),
82        &effective_skills,
83        &request.extra_skills,
84        &policy.routing.harness,
85        &policy.routing.model_token,
86        &policy.routing.model,
87    )?;
88
89    warnings.extend(prompt.warnings);
90    let resolved_tools = resolve_bundle_tools(&profile, &policy.routing.harness)?;
91
92    Ok(LaunchBundle {
93        version: 1,
94        agent: request.agent,
95        routing: policy.routing,
96        execution_policy: policy.execution_policy,
97        prompt_surface: bundle::PromptSurface {
98            system_instruction: prompt.system_instruction,
99            supplemental_documents: prompt.supplemental_documents,
100            inventory_prompt: prompt.inventory_prompt,
101        },
102        scaffold_slots: ScaffoldSlots::placeholders(),
103        tools: resolved_tools,
104        skills_metadata: SkillsMetadata {
105            loaded: prompt.loaded_skills,
106            missing: prompt.missing_skills,
107        },
108        provenance: policy.provenance,
109        warnings,
110    })
111}
112
113fn agent_file_path(project_root: &std::path::Path, agent: &str) -> PathBuf {
114    project_root
115        .join(".mars")
116        .join("agents")
117        .join(format!("{agent}.md"))
118}
119
120fn resolve_bundle_tools(
121    profile: &crate::compiler::agents::AgentProfile,
122    harness: &str,
123) -> Result<ToolsSpec, MarsError> {
124    let harness_kind = parse_harness_kind(harness)?;
125
126    let effective_tools = profile.effective_tool_policy(&harness_kind);
127
128    Ok(ToolsSpec {
129        allowed: effective_tools.allowed,
130        disallowed: effective_tools.disallowed,
131        mcp: effective_tools.mcp,
132    })
133}
134
135fn resolve_effective_skills(
136    profile: &crate::compiler::agents::AgentProfile,
137    harness: &str,
138) -> Result<Vec<String>, MarsError> {
139    let harness_kind = parse_harness_kind(harness)?;
140    Ok(profile.effective_skills(&harness_kind).to_vec())
141}
142
143fn parse_harness_kind(harness: &str) -> Result<HarnessKind, MarsError> {
144    HarnessKind::from_str(harness).ok_or_else(|| {
145        MarsError::Config(ConfigError::Invalid {
146            message: format!(
147                "invalid harness `{harness}` for launch bundle resolution; expected one of: claude, codex, opencode, cursor, pi"
148            ),
149        })
150    })
151}