fix: Allow snake case for skill name

Kebab-case is the standard for skill names/directories given by the Skill spec. However, to enable support for importing within skill scripts (python does not allow imports with kebab-case, only snake case), allow snake case for the skill name.

Co-authored-by: Kathy Wu <wukathy@google.com>
PiperOrigin-RevId: 886351132
Change-Id: Ifb6c60603aaa00a399e4f5f14cd51c35737c25c1
This commit is contained in:
Kathy Wu
2026-03-19 13:39:22 -07:00
committed by George Weale
parent 824fcda429
commit 52250da81f
3 changed files with 55 additions and 8 deletions
@@ -54,6 +54,7 @@ class FeatureName(str, Enum):
TOOL_CONFIG = "TOOL_CONFIG"
TOOL_CONFIRMATION = "TOOL_CONFIRMATION"
PLUGGABLE_AUTH = "PLUGGABLE_AUTH"
SNAKE_CASE_SKILL_NAME = "SNAKE_CASE_SKILL_NAME"
V1_LLM_AGENT = "V1_LLM_AGENT"
@@ -174,6 +175,9 @@ _FEATURE_REGISTRY: dict[FeatureName, FeatureConfig] = {
FeatureName.PLUGGABLE_AUTH: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=True
),
FeatureName.SNAKE_CASE_SKILL_NAME: FeatureConfig(
FeatureStage.EXPERIMENTAL, default_on=False
),
}
# Track which experimental features have already warned (warn only once)
+24 -6
View File
@@ -26,14 +26,20 @@ from pydantic import ConfigDict
from pydantic import Field
from pydantic import field_validator
_NAME_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
from ..features import FeatureName
from ..features import is_feature_enabled
_KEBAB_NAME_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
_SNAKE_OR_KEBAB_NAME_PATTERN = re.compile(
r"^([a-z0-9]+(-[a-z0-9]+)*|[a-z0-9]+(_[a-z0-9]+)*)$"
)
class Frontmatter(BaseModel):
"""L1 skill content: metadata parsed from SKILL.md for skill discovery.
Attributes:
name: Skill name in kebab-case (required).
name: Skill name in kebab-case or snake_case (required).
description: What the skill does and when the model should use it
(required).
license: License for the skill (optional).
@@ -78,11 +84,23 @@ class Frontmatter(BaseModel):
v = unicodedata.normalize("NFKC", v)
if len(v) > 64:
raise ValueError("name must be at most 64 characters")
if not _NAME_PATTERN.match(v):
raise ValueError(
"name must be lowercase kebab-case (a-z, 0-9, hyphens),"
" with no leading, trailing, or consecutive hyphens"
if is_feature_enabled(FeatureName.SNAKE_CASE_SKILL_NAME):
pattern = _SNAKE_OR_KEBAB_NAME_PATTERN
msg = (
"name must be lowercase kebab-case (a-z, 0-9, hyphens) or"
" snake_case (a-z, 0-9, underscores), with no leading, trailing,"
" or consecutive delimiters. Mixing hyphens and underscores is"
" not allowed."
)
else:
pattern = _KEBAB_NAME_PATTERN
msg = (
"name must be lowercase kebab-case (a-z, 0-9,"
" hyphens), with no leading, trailing, or"
" consecutive delimiters"
)
if not pattern.match(v):
raise ValueError(msg)
return v
@field_validator("description")
+27 -2
View File
@@ -14,6 +14,8 @@
"""Unit tests for skill models."""
from google.adk.features import FeatureName
from google.adk.features._feature_registry import temporary_feature_override
from google.adk.skills import models
from pydantic import ValidationError
import pytest
@@ -99,16 +101,39 @@ def test_name_consecutive_hyphens():
models.Frontmatter(name="my--skill", description="desc")
def test_name_invalid_chars_underscore():
def test_name_underscore_rejected_by_default():
with pytest.raises(ValidationError, match="lowercase kebab-case"):
models.Frontmatter(name="my_skill", description="desc")
def test_name_valid_underscore_preserved_with_flag():
with temporary_feature_override(FeatureName.SNAKE_CASE_SKILL_NAME, True):
fm = models.Frontmatter(name="my_skill", description="desc")
assert fm.name == "my_skill"
def test_name_invalid_chars_ampersand():
with pytest.raises(ValidationError, match="lowercase kebab-case"):
with pytest.raises(
ValidationError, match="name must be lowercase kebab-case"
):
models.Frontmatter(name="skill&name", description="desc")
def test_name_mixed_delimiters_rejected_by_default():
with pytest.raises(
ValidationError, match="name must be lowercase kebab-case"
):
models.Frontmatter(name="my-skill_1", description="desc")
def test_name_mixed_delimiters_rejected_with_flag():
with temporary_feature_override(FeatureName.SNAKE_CASE_SKILL_NAME, True):
with pytest.raises(
ValidationError, match="Mixing hyphens and underscores is not allowed"
):
models.Frontmatter(name="my-skill_1", description="desc")
def test_name_valid_passes():
fm = models.Frontmatter(name="my-skill-2", description="desc")
assert fm.name == "my-skill-2"