diff --git a/agentscope-core/src/main/java/io/agentscope/core/skill/util/SkillFileSystemHelper.java b/agentscope-core/src/main/java/io/agentscope/core/skill/util/SkillFileSystemHelper.java index daccd99a6f..39d3b586ff 100644 --- a/agentscope-core/src/main/java/io/agentscope/core/skill/util/SkillFileSystemHelper.java +++ b/agentscope-core/src/main/java/io/agentscope/core/skill/util/SkillFileSystemHelper.java @@ -126,10 +126,22 @@ public static AgentSkill loadSkillFromDirectory( /** * Retrieves all skill names by parsing SKILL.md metadata in each skill folder. * + *

If {@code baseDir} itself contains a {@code SKILL.md} file, it is treated as a single + * skill directory and only that skill's name is returned. Otherwise, subdirectories are + * scanned for skills. + * * @param baseDir The base directory containing skill folders * @return A list of skill names sorted alphabetically */ public static List getAllSkillNames(Path baseDir) { + // If baseDir itself is a skill directory, return only that skill + if (hasSkillFile(baseDir)) { + List names = new ArrayList<>(); + readSkillName(baseDir).ifPresent(names::add); + names.sort(String::compareTo); + return names; + } + List skillNames = new ArrayList<>(); try (Stream subdirs = Files.list(baseDir)) { @@ -160,6 +172,10 @@ public static List getAllSkills(Path baseDir, String source) { /** * Retrieves all skills from the base directory. * + *

If {@code baseDir} itself contains a {@code SKILL.md} file, it is treated as a single + * skill directory and only that skill is returned. Otherwise, subdirectories are scanned for + * skills. + * * @param baseDir The base directory containing skill folders * @param source The source identifier for created skills * @param includeResources when {@code false}, each skill's resource map is left empty and @@ -168,6 +184,17 @@ public static List getAllSkills(Path baseDir, String source) { */ public static List getAllSkills( Path baseDir, String source, boolean includeResources) { + // If baseDir itself is a skill directory, return only that skill + if (hasSkillFile(baseDir)) { + List skills = new ArrayList<>(); + try { + skills.add(loadSkillFromDirectory(baseDir, source, includeResources)); + } catch (Exception e) { + logger.warn("Failed to load skill from '{}': {}", baseDir, e.getMessage(), e); + } + return skills; + } + List skills = new ArrayList<>(); try (Stream subdirs = Files.list(baseDir)) { @@ -463,6 +490,16 @@ private static Optional findSkillDirectoryByName(Path baseDir, String skil return Optional.empty(); } + // Check if baseDir itself is the skill + if (hasSkillFile(baseDir)) { + String rootName = readSkillName(baseDir).orElse(null); + if (skillName.equals(rootName)) { + return Optional.of(baseDir); + } + // baseDir is a single skill directory; no subdirectory skills to scan + return Optional.empty(); + } + try (Stream subdirs = Files.list(baseDir)) { return subdirs.filter(Files::isDirectory) .filter(SkillFileSystemHelper::hasSkillFile) diff --git a/agentscope-core/src/test/java/io/agentscope/core/skill/util/SkillFileSystemHelperTest.java b/agentscope-core/src/test/java/io/agentscope/core/skill/util/SkillFileSystemHelperTest.java index 21c4da5e0c..c8be4e3201 100644 --- a/agentscope-core/src/test/java/io/agentscope/core/skill/util/SkillFileSystemHelperTest.java +++ b/agentscope-core/src/test/java/io/agentscope/core/skill/util/SkillFileSystemHelperTest.java @@ -494,6 +494,88 @@ void shouldFilterOsHiddenFilesOnWindows() throws IOException { "OS hidden file should be filtered out on Windows"); } + @Test + @DisplayName("Should return root skill name when baseDir itself has SKILL.md") + void testGetAllSkillNames_RootLevelSkill() throws IOException { + Path rootSkillDir = tempDir.resolve("root-skill-dir"); + Files.createDirectories(rootSkillDir); + Files.writeString( + rootSkillDir.resolve("SKILL.md"), + "---\nname: root-skill\ndescription: Root Skill\n---\nRoot content", + StandardCharsets.UTF_8); + // Add a subdirectory (e.g. references/) that should be ignored + Files.createDirectories(rootSkillDir.resolve("references")); + Files.writeString(rootSkillDir.resolve("references/doc.md"), "doc"); + + List names = SkillFileSystemHelper.getAllSkillNames(rootSkillDir); + assertEquals(1, names.size()); + assertEquals("root-skill", names.get(0)); + } + + @Test + @DisplayName("Should return root skill when baseDir itself has SKILL.md") + void testGetAllSkills_RootLevelSkill() throws IOException { + Path rootSkillDir = tempDir.resolve("root-skill-dir2"); + Files.createDirectories(rootSkillDir); + Files.writeString( + rootSkillDir.resolve("SKILL.md"), + "---\nname: root-skill2\ndescription: Root Skill 2\n---\nRoot content 2", + StandardCharsets.UTF_8); + Files.writeString(rootSkillDir.resolve("extra.txt"), "extra resource"); + + List skills = SkillFileSystemHelper.getAllSkills(rootSkillDir, "git-source"); + assertEquals(1, skills.size()); + assertEquals("root-skill2", skills.get(0).getName()); + assertEquals("git-source", skills.get(0).getSource()); + assertTrue(skills.get(0).getResources().containsKey("extra.txt")); + } + + @Test + @DisplayName("Should load root skill by name when baseDir itself has SKILL.md") + void testLoadSkill_RootLevelSkill() throws IOException { + Path rootSkillDir = tempDir.resolve("root-skill-dir3"); + Files.createDirectories(rootSkillDir); + Files.writeString( + rootSkillDir.resolve("SKILL.md"), + "---\nname: root-skill3\ndescription: Root Skill 3\n---\nRoot content 3", + StandardCharsets.UTF_8); + + AgentSkill skill = + SkillFileSystemHelper.loadSkill(rootSkillDir, "root-skill3", "git-source"); + assertNotNull(skill); + assertEquals("root-skill3", skill.getName()); + assertEquals("Root Skill 3", skill.getDescription()); + } + + @Test + @DisplayName("Should find root skill via skillExists when baseDir itself has SKILL.md") + void testSkillExists_RootLevelSkill() throws IOException { + Path rootSkillDir = tempDir.resolve("root-skill-dir4"); + Files.createDirectories(rootSkillDir); + Files.writeString( + rootSkillDir.resolve("SKILL.md"), + "---\nname: root-skill4\ndescription: Root Skill 4\n---\nContent", + StandardCharsets.UTF_8); + + assertTrue(SkillFileSystemHelper.skillExists(rootSkillDir, "root-skill4")); + assertFalse(SkillFileSystemHelper.skillExists(rootSkillDir, "nonexistent")); + } + + @Test + @DisplayName("Should throw when root skill name does not match requested name") + void testLoadSkill_RootLevelSkillNameMismatch() throws IOException { + Path rootSkillDir = tempDir.resolve("root-skill-dir5"); + Files.createDirectories(rootSkillDir); + Files.writeString( + rootSkillDir.resolve("SKILL.md"), + "---\nname: actual-name\ndescription: Actual\n---\nContent", + StandardCharsets.UTF_8); + + assertThrows( + IllegalArgumentException.class, + () -> SkillFileSystemHelper.loadSkill(rootSkillDir, "wrong-name", "source")); + } + private void createSampleSkill(String name, String description, String content) throws IOException { Path skillDir = skillsBaseDir.resolve(name);