Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
28 changes: 28 additions & 0 deletions docs/edge/en/concepts/skills.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,34 @@ agent = Agent(
)
```

### Pin a Version

An unpinned reference resolves to the newest published version, so publishing a
new version changes every agent that references it. Append `@<version>` to pin
one instead:

```python
agent = Agent(
role="Senior Code Reviewer",
goal="Review pull requests for quality and security issues",
backstory="Staff engineer with expertise in secure coding practices.",
skills=["@acme/code-review@1.2.0"], # pinned; a leading "v" also works
)
```

A pinned reference re-downloads unless the copy it finds is that exact version —
a pin asks for a specific version rather than hinting at one. A cached skill is
matched on the version recorded when it was installed, so it needs nothing in
its frontmatter; a project-local copy under `skills/` has no such record, so it
is matched on `metadata.version` in its `SKILL.md` frontmatter. Pinning an
unpublished version fails rather than falling back to the latest.

<Note>
Agents from the **Agent Repository** are pinned automatically: the repository
records a version alongside each skill it assigns, and the runtime applies those
pins when it loads the agent.
</Note>

### List

```shell Terminal
Expand Down
4 changes: 4 additions & 0 deletions lib/crewai/src/crewai/experimental/skills/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
from crewai.skills import cache, events, registry
from crewai.skills.cache import SkillCacheManager
from crewai.skills.registry import (
SkillRef,
is_registry_ref,
parse_registry_ref,
parse_skill_ref,
resolve_registry_ref,
)

Expand All @@ -26,10 +28,12 @@

__all__ = [
"SkillCacheManager",
"SkillRef",
"cache",
"events",
"is_registry_ref",
"parse_registry_ref",
"parse_skill_ref",
"registry",
"resolve_registry_ref",
]
4 changes: 4 additions & 0 deletions lib/crewai/src/crewai/skills/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
from crewai.skills.models import Skill, SkillFrontmatter
from crewai.skills.parser import SkillParseError
from crewai.skills.registry import (
SkillRef,
is_registry_ref,
parse_registry_ref,
parse_skill_ref,
resolve_registry_ref,
)

Expand All @@ -26,11 +28,13 @@
"SkillCacheManager",
"SkillFrontmatter",
"SkillParseError",
"SkillRef",
"activate_skill",
"discover_skills",
"is_registry_ref",
"load_skill",
"load_skills",
"parse_registry_ref",
"parse_skill_ref",
"resolve_registry_ref",
]
44 changes: 39 additions & 5 deletions lib/crewai/src/crewai/skills/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from typing import TypedDict
import zipfile

from crewai.skills.validation import versions_match


_logger = logging.getLogger(__name__)

Expand All @@ -39,13 +41,45 @@ def __init__(self, cache_root: Path | None = None) -> None:
def _skill_dir(self, org: str, name: str) -> Path:
return self._root / org / name

def get_cached_path(self, org: str, name: str) -> Path | None:
"""Return the cached skill directory path if it exists, else None."""
def get_cached_path(
self, org: str, name: str, version: str | None = None
) -> Path | None:
"""Return the cached skill directory path if usable, else None.

Args:
org: Organisation slug.
name: Skill name.
version: When given, the cached entry must record this version.
The cache holds one version per skill, so a pinned lookup for a
different version reports a miss and the caller re-downloads
rather than loading the wrong version.

Returns:
The cached skill directory, or None on a miss.
"""
skill_dir = self._skill_dir(org, name)
meta_file = skill_dir / _META_FILENAME
if skill_dir.is_dir() and meta_file.exists():
return skill_dir
return None
if not (skill_dir.is_dir() and meta_file.exists()):
return None
if version is not None and not self._records_version(meta_file, version):
return None
return skill_dir

def _records_version(self, meta_file: Path, version: str) -> bool:
"""Return True when the cache metadata records *version*."""
try:
meta = json.loads(meta_file.read_text(encoding="utf-8"))
except (OSError, ValueError):
# ValueError covers both JSONDecodeError and the UnicodeDecodeError
# a non-UTF-8 file raises, so a corrupted entry reads as a miss.
_logger.debug("Unreadable cache entry: %s", meta_file, exc_info=True)
return False
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if not isinstance(meta, dict):
_logger.debug("Malformed cache entry: %s", meta_file)
return False
# versions_match() treats a non-string version as no match, so a
# corrupted entry reads as a miss rather than raising.
return versions_match(version, meta.get("version"))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
joaomdmoura marked this conversation as resolved.

def store(
self, org: str, name: str, version: str | None, archive_bytes: bytes
Expand Down
Loading
Loading