Skip to main content

bake_agent_context/
lib.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4//! Bake tasks and APIs for consuming context directories shipped by Cargo packages.
5mod document;
6mod exclude;
7mod installer;
8mod ownership;
9mod package;
10mod skill;
11#[cfg(test)]
12mod test_filesystem;
13
14pub use installer::{Definition, FileDefinition, Installation, Installer, Specification};
15pub use package::Package;
16pub use skill::SkillDefinition;
17
18use bake::{Context, Error, Result};
19
20fn installer(context: &mut Context) -> Result<Installer> {
21    if let Some(installer) = context.get::<Installer>() {
22        return Ok(installer.clone());
23    }
24
25    let installer = Installer::discover(context.root())?;
26    context.insert(installer.clone());
27    Ok(installer)
28}
29
30/// List dependency crates that ship context, or list one crate's definitions.
31#[bake::task(name = "agent:context:list")]
32pub fn list(context: &mut Context, package: Option<String>) -> Result<String> {
33    let installer = installer(context)?;
34
35    if let Some(selector) = package {
36        let Some(provider) = installer.find_provider(&selector)? else {
37            return Ok(format!("No context found for crate '{selector}'"));
38        };
39        if provider.definitions.is_empty() {
40            return Ok(format!(
41                "No context definitions found for crate '{selector}'."
42            ));
43        }
44        let mut output = format!("Context files for crate '{selector}':");
45        for definition in provider.definitions {
46            output.push_str(&format!("\n  {}", definition.display_name()));
47        }
48        return Ok(output);
49    }
50
51    let providers = installer.providers()?;
52    let providers: Vec<_> = providers
53        .iter()
54        .filter(|provider| !provider.definitions.is_empty())
55        .collect();
56    if providers.is_empty() {
57        return Ok("No dependency context files found".to_owned());
58    }
59
60    let mut output = String::from("Crates with context available:");
61    for provider in providers {
62        output.push_str(&format!(
63            "\n  {} ({})",
64            provider.specification.selector(),
65            provider.specification.version
66        ));
67    }
68    Ok(output)
69}
70
71/// Show a context file from one resolved dependency crate.
72#[bake::task(name = "agent:context:show")]
73pub fn show(
74    context: &mut Context,
75    #[bake(named)] package: String,
76    #[bake(named)] file: String,
77) -> Result<String> {
78    let installer = installer(context)?;
79    let Some(content) = installer.show_context_file(&package, &file)? else {
80        return Err(Error::new(format!(
81            "context file {file:?} was not found in crate {package:?}"
82        )));
83    };
84    Ok(content)
85}
86
87/// Install context and skills from one crate or all dependencies.
88#[bake::task(name = "agent:context:install")]
89pub fn install(context: &mut Context, package: Option<String>) -> Result<String> {
90    let installer = installer(context)?;
91    let installed = installer.install(package.as_deref())?;
92    let installed_context = installed.context;
93    let installed_skills = installed.skills;
94
95    let mut output = Vec::new();
96    if !installed_context.is_empty() {
97        output.push(format!(
98            "Installed context from: {}",
99            installed_context.join(", ")
100        ));
101    }
102    if !installed_skills.is_empty() {
103        output.push(format!("Installed skills: {}", installed_skills.join(", ")));
104    }
105
106    if output.is_empty() {
107        Ok("No dependency context or skills were installed".to_owned())
108    } else {
109        Ok(output.join("\n"))
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use std::fs;
117    use std::path::Path;
118    use tempfile::tempdir;
119
120    fn make_context(root: &Path, specifications: Vec<Specification>) -> Context {
121        let mut context = bake::Registry::new().context(root);
122        context.insert(Installer::for_test(root, specifications));
123        context
124    }
125
126    fn specification(root: &Path, name: &str, version: &str) -> Specification {
127        Specification::for_test(name, version, root.join(name).join("context"))
128    }
129
130    fn write(path: &Path, contents: &str) {
131        fs::create_dir_all(path.parent().unwrap()).unwrap();
132        fs::write(path, contents).unwrap();
133    }
134
135    #[test]
136    fn task_wrappers_return_outputs_and_propagate_errors() {
137        let directory = tempdir().unwrap();
138        let root = directory.path();
139        let guide = specification(root, "provider", "1.0.0");
140        write(&guide.context_path.join("guide.md"), "# Guide\n");
141        write(
142            &guide.context_path.join("broken.md"),
143            "---\nmetadata: [\n---\n# Invalid YAML\n",
144        );
145        let mut context = make_context(root, vec![guide.clone()]);
146
147        assert!(
148            list(&mut context, Some("unknown".to_owned()))
149                .unwrap()
150                .contains("No context found")
151        );
152        assert!(list(&mut context, None).is_err());
153        assert!(list(&mut context, Some("provider".to_owned())).is_err());
154        assert!(install(&mut context, None).is_err());
155        assert!(show(&mut context, "provider".to_owned(), "missing.md".to_owned()).is_err());
156        assert!(install(&mut context, Some("provider".to_owned())).is_err());
157
158        let mut duplicates = make_context(
159            root,
160            vec![
161                specification(root, "duplicate", "1.0.0"),
162                specification(root, "duplicate", "2.0.0"),
163            ],
164        );
165        assert!(list(&mut duplicates, Some("duplicate".to_owned())).is_err());
166        assert!(
167            show(
168                &mut duplicates,
169                "duplicate".to_owned(),
170                "guide.md".to_owned()
171            )
172            .is_err()
173        );
174        assert!(
175            duplicates
176                .get::<Installer>()
177                .unwrap()
178                .install(Some("duplicate"))
179                .is_err()
180        );
181
182        let listing_package = specification(root, "listing", "1.0.0");
183        write(
184            &listing_package.context_path.join("guide.md"),
185            "# Listing\n",
186        );
187        let mut listing_context = make_context(root, vec![listing_package]);
188        assert!(
189            list(&mut listing_context, None)
190                .unwrap()
191                .contains("listing@1.0.0 (1.0.0)")
192        );
193
194        let empty_package = specification(root, "empty", "1.0.0");
195        fs::create_dir_all(&empty_package.context_path).unwrap();
196        let mut empty_context = make_context(root, vec![empty_package]);
197        assert_eq!(
198            list(&mut empty_context, Some("empty".to_owned())).unwrap(),
199            "No context definitions found for crate 'empty'."
200        );
201        assert!(
202            list(&mut empty_context, None)
203                .unwrap()
204                .contains("No dependency context files")
205        );
206    }
207
208    #[test]
209    fn task_wrappers_propagate_install_failures() {
210        let directory = tempdir().unwrap();
211        let root = directory.path();
212        let specification = specification(root, "provider", "1.0.0");
213        write(&specification.context_path.join("guide.md"), "# Guide\n");
214
215        let ownership = root.join(".agents/skills/provider-workflow/SKILL.md");
216        write(
217            &ownership,
218            "---\nmetadata:\n  type: skill\n  ecosystem: cargo\n  package: provider\n  version: 1.0.0\n---\n",
219        );
220        let failure_path = ownership.clone();
221        let _failure = test_filesystem::fail_once(test_filesystem::Operation::Read, move |path| {
222            path == failure_path
223        });
224        let mut context = make_context(root, vec![specification.clone()]);
225        assert!(install(&mut context, Some("provider".to_owned())).is_err());
226        drop(_failure);
227
228        let mut empty = make_context(root, Vec::new());
229        assert!(
230            list(&mut empty, None)
231                .unwrap()
232                .contains("No dependency context files")
233        );
234    }
235
236    #[test]
237    fn installer_creation_errors_are_returned_by_tasks() {
238        let directory = tempdir().unwrap();
239        let mut context = bake::Registry::new().context(directory.path());
240        assert!(list(&mut context, None).is_err());
241        assert!(show(&mut context, "provider".to_owned(), "guide.md".to_owned()).is_err());
242        assert!(install(&mut context, None).is_err());
243    }
244}