Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions src/google/adk/skills/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,40 @@
})


def _resolve_skill_dir_path(skill_dir: Union[str, pathlib.Path]) -> pathlib.Path:
"""Resolve a skill path consistently across execution environments.

Relative skill paths may be authored from a parent folder (for example,
``agent_name/skills/my-skill``) while execution happens with cwd already set
to ``agent_name``. In that case, naively resolving against cwd produces a
duplicated segment (``agent_name/agent_name/...``).

Args:
skill_dir: Raw skill directory path provided by caller.

Returns:
A best-effort resolved path.
"""
path = pathlib.Path(skill_dir)
if path.is_absolute():
return path.resolve()

cwd = pathlib.Path.cwd()
candidates = []

if path.parts and path.parts[0] == cwd.name:
stripped = pathlib.Path(*path.parts[1:])
candidates.append(cwd / stripped)

candidates.append(cwd / path)

for candidate in candidates:
if candidate.exists():
return candidate.resolve()

return candidates[-1].resolve()


def _load_dir(directory: pathlib.Path) -> dict[str, str]:
"""Recursively load files from a directory into a dictionary.

Expand Down Expand Up @@ -122,7 +156,7 @@ def _load_skill_from_dir(skill_dir: Union[str, pathlib.Path]) -> models.Skill:
ValueError: If SKILL.md is invalid or the skill name does not match
the directory name.
"""
skill_dir = pathlib.Path(skill_dir).resolve()
skill_dir = _resolve_skill_dir_path(skill_dir)

parsed, body, skill_md = _parse_skill_md(skill_dir)

Expand Down Expand Up @@ -171,7 +205,7 @@ def _validate_skill_dir(
List of problem strings. Empty list means the skill is valid.
"""
problems: list[str] = []
skill_dir = pathlib.Path(skill_dir).resolve()
skill_dir = _resolve_skill_dir_path(skill_dir)

if not skill_dir.exists():
return [f"Directory '{skill_dir}' does not exist."]
Expand Down Expand Up @@ -229,6 +263,6 @@ def _read_skill_properties(
FileNotFoundError: If the directory or SKILL.md is not found.
ValueError: If the frontmatter is invalid.
"""
skill_dir = pathlib.Path(skill_dir).resolve()
skill_dir = _resolve_skill_dir_path(skill_dir)
parsed, _, _ = _parse_skill_md(skill_dir)
return models.Frontmatter.model_validate(parsed)
24 changes: 24 additions & 0 deletions tests/unittests/skills/test__utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,27 @@ def test__read_skill_properties(tmp_path):
assert fm.name == "my-skill"
assert fm.description == "A cool skill"
assert fm.license == "MIT"


def test_load_skill_from_dir_resolves_duplicate_agent_prefix(tmp_path, monkeypatch):
"""Resolves agent-prefixed relative paths when cwd is already the agent dir."""
workspace_dir = tmp_path / "workspace"
agent_dir = workspace_dir / "my-agent"
skill_dir = agent_dir / "skills" / "my-skill"
skill_dir.mkdir(parents=True)

skill_md = """---
name: my-skill
description: Prefix path test
---
Body
"""
(skill_dir / "SKILL.md").write_text(skill_md)

monkeypatch.chdir(agent_dir)

# This path is valid from workspace root but commonly passed from agent code.
skill = _load_skill_from_dir("my-agent/skills/my-skill")

assert skill.name == "my-skill"
assert skill.description == "Prefix path test"