Skip to content

Commit 3ceab49

Browse files
committed
fix(skills): resolve agent-prefixed relative skill paths
1 parent feefadf commit 3ceab49

2 files changed

Lines changed: 61 additions & 3 deletions

File tree

src/google/adk/skills/_utils.py

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,40 @@
3434
})
3535

3636

37+
def _resolve_skill_dir_path(skill_dir: Union[str, pathlib.Path]) -> pathlib.Path:
38+
"""Resolve a skill path consistently across execution environments.
39+
40+
Relative skill paths may be authored from a parent folder (for example,
41+
``agent_name/skills/my-skill``) while execution happens with cwd already set
42+
to ``agent_name``. In that case, naively resolving against cwd produces a
43+
duplicated segment (``agent_name/agent_name/...``).
44+
45+
Args:
46+
skill_dir: Raw skill directory path provided by caller.
47+
48+
Returns:
49+
A best-effort resolved path.
50+
"""
51+
path = pathlib.Path(skill_dir)
52+
if path.is_absolute():
53+
return path.resolve()
54+
55+
cwd = pathlib.Path.cwd()
56+
candidates = [cwd / path]
57+
58+
if path.parts and path.parts[0] == cwd.name:
59+
stripped = pathlib.Path(*path.parts[1:])
60+
candidates.append(cwd / stripped)
61+
62+
candidates.append(cwd.parent / path)
63+
64+
for candidate in candidates:
65+
if candidate.exists():
66+
return candidate.resolve()
67+
68+
return candidates[0].resolve()
69+
70+
3771
def _load_dir(directory: pathlib.Path) -> dict[str, str]:
3872
"""Recursively load files from a directory into a dictionary.
3973
@@ -122,7 +156,7 @@ def _load_skill_from_dir(skill_dir: Union[str, pathlib.Path]) -> models.Skill:
122156
ValueError: If SKILL.md is invalid or the skill name does not match
123157
the directory name.
124158
"""
125-
skill_dir = pathlib.Path(skill_dir).resolve()
159+
skill_dir = _resolve_skill_dir_path(skill_dir)
126160

127161
parsed, body, skill_md = _parse_skill_md(skill_dir)
128162

@@ -171,7 +205,7 @@ def _validate_skill_dir(
171205
List of problem strings. Empty list means the skill is valid.
172206
"""
173207
problems: list[str] = []
174-
skill_dir = pathlib.Path(skill_dir).resolve()
208+
skill_dir = _resolve_skill_dir_path(skill_dir)
175209

176210
if not skill_dir.exists():
177211
return [f"Directory '{skill_dir}' does not exist."]
@@ -229,6 +263,6 @@ def _read_skill_properties(
229263
FileNotFoundError: If the directory or SKILL.md is not found.
230264
ValueError: If the frontmatter is invalid.
231265
"""
232-
skill_dir = pathlib.Path(skill_dir).resolve()
266+
skill_dir = _resolve_skill_dir_path(skill_dir)
233267
parsed, _, _ = _parse_skill_md(skill_dir)
234268
return models.Frontmatter.model_validate(parsed)

tests/unittests/skills/test__utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,27 @@ def test__read_skill_properties(tmp_path):
180180
assert fm.name == "my-skill"
181181
assert fm.description == "A cool skill"
182182
assert fm.license == "MIT"
183+
184+
185+
def test_load_skill_from_dir_resolves_duplicate_agent_prefix(tmp_path, monkeypatch):
186+
"""Resolves agent-prefixed relative paths when cwd is already the agent dir."""
187+
workspace_dir = tmp_path / "workspace"
188+
agent_dir = workspace_dir / "my-agent"
189+
skill_dir = agent_dir / "skills" / "my-skill"
190+
skill_dir.mkdir(parents=True)
191+
192+
skill_md = """---
193+
name: my-skill
194+
description: Prefix path test
195+
---
196+
Body
197+
"""
198+
(skill_dir / "SKILL.md").write_text(skill_md)
199+
200+
monkeypatch.chdir(agent_dir)
201+
202+
# This path is valid from workspace root but commonly passed from agent code.
203+
skill = _load_skill_from_dir("my-agent/skills/my-skill")
204+
205+
assert skill.name == "my-skill"
206+
assert skill.description == "Prefix path test"

0 commit comments

Comments
 (0)