diff --git a/src/google/adk/skills/_utils.py b/src/google/adk/skills/_utils.py index 0bfbf30ef6..7b611fafc7 100644 --- a/src/google/adk/skills/_utils.py +++ b/src/google/adk/skills/_utils.py @@ -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. @@ -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) @@ -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."] @@ -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) diff --git a/tests/unittests/skills/test__utils.py b/tests/unittests/skills/test__utils.py index 5a65648dbb..5f70faf565 100644 --- a/tests/unittests/skills/test__utils.py +++ b/tests/unittests/skills/test__utils.py @@ -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"