diff --git a/agentscope-core/pom.xml b/agentscope-core/pom.xml
index 54bf500a7..ed1f99515 100644
--- a/agentscope-core/pom.xml
+++ b/agentscope-core/pom.xml
@@ -140,5 +140,11 @@
com.networknt
json-schema-validator
+
+
+
+ org.yaml
+ snakeyaml
+
diff --git a/agentscope-core/src/main/java/io/agentscope/core/skill/util/MarkdownSkillParser.java b/agentscope-core/src/main/java/io/agentscope/core/skill/util/MarkdownSkillParser.java
index 5bce28ced..faca2b958 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/skill/util/MarkdownSkillParser.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/skill/util/MarkdownSkillParser.java
@@ -20,6 +20,9 @@
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.yaml.snakeyaml.LoaderOptions;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.SafeConstructor;
/**
* Utility for parsing and generating Markdown files with YAML frontmatter.
@@ -145,22 +148,19 @@ public static String generate(Map metadata, String content) {
}
/**
- * Simple YAML parser for flat key-value structures.
- * Only supports String:String mappings.
+ * YAML parser that tolerates standard frontmatter syntax while exposing only top-level scalar
+ * metadata values to the rest of the skill system.
*/
private static class SimpleYamlParser {
-
- // Pattern to match key: value format
- // Captures: group(1) = key, group(2) = value (may include quotes)
- private static final Pattern KEY_VALUE_PATTERN =
- Pattern.compile("^([a-zA-Z_][a-zA-Z0-9_-]*)\\s*:\\s*(.*)$");
+ private static final Yaml YAML = new Yaml(new SafeConstructor(new LoaderOptions()));
/**
* Parse YAML string into a map of key-value pairs.
*
* @param yaml YAML content to parse
* @return Map of key-value pairs
- * @throws IllegalArgumentException if YAML syntax is invalid
+ * @throws IllegalArgumentException if YAML syntax is invalid or the top-level value is not a
+ * mapping
*/
static Map parse(String yaml) {
Map result = new LinkedHashMap<>();
@@ -169,116 +169,54 @@ static Map parse(String yaml) {
return result;
}
- String[] lines = yaml.split("[\\r\\n]+");
+ Object parsed = YAML.load(yaml);
+ if (parsed == null) {
+ return result;
+ }
- for (String line : lines) {
- // Skip empty lines
- if (line.trim().isEmpty()) {
- continue;
- }
+ if (!(parsed instanceof Map, ?> parsedMap)) {
+ throw new IllegalArgumentException(
+ "Invalid YAML frontmatter: expected a top-level mapping");
+ }
- // Skip comments
- if (line.trim().startsWith("#")) {
+ for (Map.Entry, ?> entry : parsedMap.entrySet()) {
+ Object rawKey = entry.getKey();
+ if (rawKey == null) {
continue;
}
- Matcher matcher = KEY_VALUE_PATTERN.matcher(line.trim());
- if (!matcher.matches()) {
- throw new IllegalArgumentException(
- "Invalid YAML line (expected 'key: value' format): " + line);
+ Object value = entry.getValue();
+ if (isScalarValue(value)) {
+ result.put(String.valueOf(rawKey), scalarToString(value));
}
-
- String key = matcher.group(1);
- String value = parseValue(matcher.group(2));
-
- result.put(key, value);
}
-
return result;
}
/**
- * Parse a YAML value, handling quoted strings.
+ * Check whether a parsed YAML value should be exposed as metadata.
*
- * @param rawValue Raw value string from YAML
- * @return Parsed value with quotes removed if present
+ * @param value Parsed YAML value
+ * @return true if the value is a scalar that can be represented as a string
*/
- private static String parseValue(String rawValue) {
- if (rawValue == null) {
- return "";
- }
-
- String value = rawValue.trim();
-
- if (value.isEmpty()) {
- return "";
- }
-
- // Handle double-quoted strings
- if (value.startsWith("\"") && value.endsWith("\"") && value.length() >= 2) {
- return unescapeString(value.substring(1, value.length() - 1));
- }
-
- // Handle single-quoted strings
- if (value.startsWith("'") && value.endsWith("'") && value.length() >= 2) {
- // Single-quoted strings don't process escapes, except '' for '
- return value.substring(1, value.length() - 1).replace("''", "'");
- }
-
- return value;
+ private static boolean isScalarValue(Object value) {
+ return value == null
+ || value instanceof String
+ || value instanceof Number
+ || value instanceof Boolean
+ || value instanceof Character
+ || value instanceof Enum>;
}
/**
- * Unescape a double-quoted YAML string.
+ * Convert a parsed YAML scalar value into the string representation expected by
+ * ParsedMarkdown metadata.
*
- * @param str String content without surrounding quotes
- * @return Unescaped string
+ * @param value Parsed YAML scalar
+ * @return Metadata string value
*/
- private static String unescapeString(String str) {
- if (str == null || str.isEmpty()) {
- return str;
- }
-
- StringBuilder result = new StringBuilder();
- boolean escape = false;
-
- for (int i = 0; i < str.length(); i++) {
- char c = str.charAt(i);
-
- if (escape) {
- switch (c) {
- case 'n':
- result.append('\n');
- break;
- case 't':
- result.append('\t');
- break;
- case 'r':
- result.append('\r');
- break;
- case '\\':
- result.append('\\');
- break;
- case '"':
- result.append('"');
- break;
- default:
- result.append('\\').append(c);
- }
- escape = false;
- } else if (c == '\\') {
- escape = true;
- } else {
- result.append(c);
- }
- }
-
- // Handle trailing backslash
- if (escape) {
- result.append('\\');
- }
-
- return result.toString();
+ private static String scalarToString(Object value) {
+ return value == null ? "" : String.valueOf(value);
}
/**
diff --git a/agentscope-core/src/test/java/io/agentscope/core/skill/util/MarkdownSkillParserTest.java b/agentscope-core/src/test/java/io/agentscope/core/skill/util/MarkdownSkillParserTest.java
index 3d4b642b4..eda8441f3 100644
--- a/agentscope-core/src/test/java/io/agentscope/core/skill/util/MarkdownSkillParserTest.java
+++ b/agentscope-core/src/test/java/io/agentscope/core/skill/util/MarkdownSkillParserTest.java
@@ -24,7 +24,6 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import io.agentscope.core.skill.util.MarkdownSkillParser.ParsedMarkdown;
-import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
@@ -291,8 +290,7 @@ void testInvalidYaml() {
assertThrows(
IllegalArgumentException.class,
() -> MarkdownSkillParser.parse(markdown));
- assertTrue(exception.getMessage().contains("Invalid YAML line"));
- assertTrue(exception.getMessage().contains("expected 'key: value' format"));
+ assertTrue(exception.getMessage().contains("Invalid YAML frontmatter"));
}
@Test
@@ -304,7 +302,48 @@ void testListFormat() {
assertThrows(
IllegalArgumentException.class,
() -> MarkdownSkillParser.parse(markdown));
- assertTrue(exception.getMessage().contains("Invalid YAML line"));
+ assertTrue(exception.getMessage().contains("top-level mapping"));
+ }
+
+ @Test
+ @DisplayName("Should tolerate nested YAML fields and keep scalar metadata")
+ void testNestedYamlFields() {
+ String markdown =
+ "---\n"
+ + "name: test-skill\n"
+ + "description: Simple description\n"
+ + "references:\n"
+ + " - https://example.com/spec\n"
+ + "metadata:\n"
+ + " owner: platform\n"
+ + "---\n"
+ + "Content";
+
+ ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown);
+
+ assertEquals("test-skill", parsed.getMetadata().get("name"));
+ assertEquals("Simple description", parsed.getMetadata().get("description"));
+ assertFalse(parsed.getMetadata().containsKey("references"));
+ assertFalse(parsed.getMetadata().containsKey("metadata"));
+ }
+
+ @Test
+ @DisplayName("Should parse multiline scalar values")
+ void testMultilineScalarValues() {
+ String markdown =
+ "---\n"
+ + "name: test-skill\n"
+ + "description: |\n"
+ + " First line\n"
+ + " Second line\n"
+ + "---\n"
+ + "Content";
+
+ ParsedMarkdown parsed = MarkdownSkillParser.parse(markdown);
+
+ assertEquals("test-skill", parsed.getMetadata().get("name"));
+ assertTrue(parsed.getMetadata().get("description").contains("First line"));
+ assertTrue(parsed.getMetadata().get("description").contains("Second line"));
}
}
@@ -498,7 +537,7 @@ void testGetters() {
@Test
@DisplayName("Should maintain immutability")
void testImmutability() {
- Map originalMetadata = new HashMap<>();
+ Map originalMetadata = new java.util.HashMap<>();
originalMetadata.put("key", "value");
ParsedMarkdown parsed = new ParsedMarkdown(originalMetadata, "content");