From 3ceab498454a07f4500f6d9ac47cb3284a3b2127 Mon Sep 17 00:00:00 2001 From: pandego <7780875+pandego@users.noreply.github.com> Date: Tue, 3 Mar 2026 03:04:34 +0100 Subject: [PATCH 1/4] fix(skills): resolve agent-prefixed relative skill paths --- src/google/adk/skills/_utils.py | 40 +++++++++++++++++++++++++-- tests/unittests/skills/test__utils.py | 24 ++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/google/adk/skills/_utils.py b/src/google/adk/skills/_utils.py index 0bfbf30ef6..7e9bdb469b 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 = [cwd / path] + + if path.parts and path.parts[0] == cwd.name: + stripped = pathlib.Path(*path.parts[1:]) + candidates.append(cwd / stripped) + + candidates.append(cwd.parent / path) + + for candidate in candidates: + if candidate.exists(): + return candidate.resolve() + + return candidates[0].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" From 3cbaeb70ec2ec792b40328732820adf4da9431d2 Mon Sep 17 00:00:00 2001 From: pandego <7780875+pandego@users.noreply.github.com> Date: Tue, 3 Mar 2026 09:00:28 +0100 Subject: [PATCH 2/4] fix(skills): prioritize stripped relative path resolution --- src/google/adk/skills/_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/google/adk/skills/_utils.py b/src/google/adk/skills/_utils.py index 7e9bdb469b..7b611fafc7 100644 --- a/src/google/adk/skills/_utils.py +++ b/src/google/adk/skills/_utils.py @@ -53,19 +53,19 @@ def _resolve_skill_dir_path(skill_dir: Union[str, pathlib.Path]) -> pathlib.Path return path.resolve() cwd = pathlib.Path.cwd() - candidates = [cwd / path] + candidates = [] if path.parts and path.parts[0] == cwd.name: stripped = pathlib.Path(*path.parts[1:]) candidates.append(cwd / stripped) - candidates.append(cwd.parent / path) + candidates.append(cwd / path) for candidate in candidates: if candidate.exists(): return candidate.resolve() - return candidates[0].resolve() + return candidates[-1].resolve() def _load_dir(directory: pathlib.Path) -> dict[str, str]: From 59a6c7c96f2204c6a524a519c15aff8142fe7727 Mon Sep 17 00:00:00 2001 From: pandego <7780875+pandego@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:02:19 +0100 Subject: [PATCH 3/4] style: apply pyink formatting for skill path fix --- src/google/adk/skills/_utils.py | 4 +++- tests/unittests/skills/test__utils.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/google/adk/skills/_utils.py b/src/google/adk/skills/_utils.py index 7b611fafc7..67bb0dce9a 100644 --- a/src/google/adk/skills/_utils.py +++ b/src/google/adk/skills/_utils.py @@ -34,7 +34,9 @@ }) -def _resolve_skill_dir_path(skill_dir: Union[str, pathlib.Path]) -> pathlib.Path: +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, diff --git a/tests/unittests/skills/test__utils.py b/tests/unittests/skills/test__utils.py index 5f70faf565..a193672e84 100644 --- a/tests/unittests/skills/test__utils.py +++ b/tests/unittests/skills/test__utils.py @@ -182,7 +182,9 @@ def test__read_skill_properties(tmp_path): assert fm.license == "MIT" -def test_load_skill_from_dir_resolves_duplicate_agent_prefix(tmp_path, monkeypatch): +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" From 0b6c1562fad9200edcbdb6fc4d33246d96b565f5 Mon Sep 17 00:00:00 2001 From: pandego <7780875+pandego@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:32:35 +0100 Subject: [PATCH 4/4] fix(skills): type annotate parse frontmatter dict for mypy --- src/google/adk/skills/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/adk/skills/_utils.py b/src/google/adk/skills/_utils.py index 67bb0dce9a..975a51bc98 100644 --- a/src/google/adk/skills/_utils.py +++ b/src/google/adk/skills/_utils.py @@ -96,7 +96,7 @@ def _load_dir(directory: pathlib.Path) -> dict[str, str]: def _parse_skill_md( skill_dir: pathlib.Path, -) -> tuple[dict, str, pathlib.Path]: +) -> tuple[dict[str, object], str, pathlib.Path]: """Parse SKILL.md from a skill directory. Args: