feat: agent skillsの互換実装

This commit is contained in:
2026-05-05 13:16:10 +09:00
parent 5acb0d4d85
commit 37065144da
10 changed files with 1044 additions and 13 deletions
+93 -2
View File
@@ -15,8 +15,8 @@ use serde::{Deserialize, Serialize};
use crate::defaults;
use crate::model::{AuthRef, ModelManifest, ReasoningControl};
use crate::{
CompactionConfig, MemoryConfig, PodManifest, PodMeta, ScopeConfig, ToolOutputLimits,
WorkerManifest,
CompactionConfig, MemoryConfig, PodManifest, PodMeta, ScopeConfig, SkillsConfig,
ToolOutputLimits, WorkerManifest,
};
/// Partial-form Pod manifest. Every field is optional; one or more
@@ -41,6 +41,9 @@ pub struct PodManifestConfig {
/// Memory subsystem opt-in. See [`MemoryConfig`].
#[serde(default)]
pub memory: Option<MemoryConfig>,
/// External Agent Skills directories. See [`crate::SkillsConfig`].
#[serde(default)]
pub skills: Option<SkillsConfig>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@@ -183,6 +186,11 @@ impl PodManifestConfig {
{
resolve_auth_file(&mut cp.auth, base);
}
if let Some(ref mut skills) = self.skills {
for dir in &mut skills.directories {
*dir = join_if_relative(base, dir);
}
}
self
}
@@ -202,10 +210,18 @@ impl PodManifestConfig {
CompactionConfigPartial::merge,
),
memory: merge_option(self.memory, upper.memory, MemoryConfig::merge),
skills: merge_option(self.skills, upper.skills, SkillsConfig::merge),
}
}
}
impl SkillsConfig {
fn merge(mut self, upper: Self) -> Self {
self.directories.extend(upper.directories);
self
}
}
impl MemoryConfig {
fn merge(self, upper: Self) -> Self {
Self {
@@ -411,6 +427,12 @@ impl TryFrom<PodManifestConfig> for PodManifest {
})
.transpose()?;
if let Some(ref skills) = cfg.skills {
for dir in &skills.directories {
ensure_absolute("skills.directories", dir)?;
}
}
Ok(PodManifest {
pod: PodMeta { name, prompt_pack },
model: cfg.model,
@@ -418,6 +440,7 @@ impl TryFrom<PodManifestConfig> for PodManifest {
scope: cfg.scope,
compaction,
memory: cfg.memory,
skills: cfg.skills,
})
}
}
@@ -461,6 +484,7 @@ mod tests {
},
compaction: None,
memory: None,
skills: None,
}
}
@@ -892,6 +916,73 @@ name = "dbg"
assert_eq!(manifest.scope.allow.len(), 1);
}
#[test]
fn skills_directories_resolved_against_base() {
let mut cfg = minimal_valid();
cfg.skills = Some(SkillsConfig {
directories: vec![PathBuf::from(".claude/skills"), PathBuf::from("/abs/elsewhere")],
});
let resolved = cfg.resolve_paths(Path::new("/workspace/proj"));
let dirs = resolved.skills.as_ref().unwrap().directories.clone();
assert_eq!(dirs[0], PathBuf::from("/workspace/proj/.claude/skills"));
assert_eq!(dirs[1], PathBuf::from("/abs/elsewhere"));
}
#[test]
fn skills_relative_path_rejected_post_resolve() {
let mut cfg = minimal_valid();
cfg.skills = Some(SkillsConfig {
directories: vec![PathBuf::from("relative/skills")],
});
let err = PodManifest::try_from(cfg).unwrap_err();
assert!(matches!(
err,
ResolveError::RelativePath {
field: "skills.directories",
..
}
));
}
#[test]
fn skills_merge_extends_directories() {
let lower = PodManifestConfig {
skills: Some(SkillsConfig {
directories: vec![PathBuf::from("/a")],
}),
..Default::default()
};
let upper = PodManifestConfig {
skills: Some(SkillsConfig {
directories: vec![PathBuf::from("/b")],
}),
..Default::default()
};
let merged = lower.merge(upper);
let dirs = merged.skills.unwrap().directories;
assert_eq!(dirs, vec![PathBuf::from("/a"), PathBuf::from("/b")]);
}
#[test]
fn from_toml_parses_skills_section() {
let toml = r#"
[pod]
name = "x"
[skills]
directories = [".claude/skills", ".cursor/skills"]
"#;
let cfg = PodManifestConfig::from_toml(toml).unwrap();
let dirs = cfg.skills.unwrap().directories;
assert_eq!(
dirs,
vec![
PathBuf::from(".claude/skills"),
PathBuf::from(".cursor/skills"),
]
);
}
#[test]
fn merge_preserves_ref() {
let lower = PodManifestConfig {
+24
View File
@@ -44,6 +44,30 @@ pub struct PodManifest {
/// memory tools registered.
#[serde(default)]
pub memory: Option<MemoryConfig>,
/// External Agent Skills (`SKILL.md`) directories to ingest as
/// Workflows in addition to the user-level `$config_dir/skills/`.
/// Each entry is a path to a skills *root* (i.e. a directory whose
/// children are individual `<name>/SKILL.md` skill bundles). Paths
/// are resolved against the manifest's base directory like other
/// path fields.
#[serde(default)]
pub skills: Option<SkillsConfig>,
}
/// External Agent Skills (`SKILL.md`) ingest configuration. Off by
/// default at the workspace level; user-level `$config_dir/skills/` is
/// always probed regardless of this field. The intent of `directories`
/// is to surface skills that already live in `.claude/skills/`,
/// `.cursor/skills/`, etc. without duplicating them under the insomnia
/// memory tree.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SkillsConfig {
/// Skills *roots*. Children of each root must be individual
/// `<name>/SKILL.md` bundles; the directory itself is not a skill.
/// Resolved against the manifest base directory before
/// [`PodManifest`] is materialised.
#[serde(default)]
pub directories: Vec<PathBuf>,
}
/// Memory subsystem configuration. Presence in the manifest enables
+6
View File
@@ -79,6 +79,12 @@ pub fn user_prompts_dir() -> Option<PathBuf> {
Some(config_dir()?.join("prompts"))
}
/// `<config_dir>/skills/` — user-level Agent Skills ライブラリ。
/// 配下は `<name>/SKILL.md` の集合として読まれる。
pub fn user_skills_dir() -> Option<PathBuf> {
Some(config_dir()?.join("skills"))
}
/// `<config_dir>/prompts.toml` — user prompt pack。
pub fn user_pack_file() -> Option<PathBuf> {
Some(config_dir()?.join("prompts.toml"))