diff --git a/src/anthropic/lib/tools/_skills.py b/src/anthropic/lib/tools/_skills.py index 0a1e9e537..8792ad7dc 100644 --- a/src/anthropic/lib/tools/_skills.py +++ b/src/anthropic/lib/tools/_skills.py @@ -274,9 +274,23 @@ async def download_session_skills( adest = anyio.Path(dest) if await adest.is_symlink(): await adest.unlink() + # If the directory already exists and contains the expected version, + # skip the download. This prevents race conditions and redundant + # work when multiple sessions share a workdir. + version_file = dest / ".version" + if await adest.is_dir() and await anyio.Path(version_file).exists(): + try: + if (await anyio.Path(version_file).read_text()).strip() == version_id: + log.info("skill skill_id=%s version=%s already exists at %s; skipping", skill.skill_id, version_id, dest) + downloaded.append(dest) + continue + except Exception: + pass + # ``shutil.rmtree`` is blocking; keep it off the event loop. await run_sync(partial(shutil.rmtree, dest, ignore_errors=True)) await _download_and_extract(client, skill.skill_id, version_id, dest) + await anyio.Path(version_file).write_text(version_id) downloaded.append(dest) log.info("downloaded skill skill_id=%s version=%s -> %s", skill.skill_id, version_id, dest) except Exception as e: diff --git a/src/anthropic/lib/tools/agent_toolset.py b/src/anthropic/lib/tools/agent_toolset.py index ccb662457..0d4d56272 100644 --- a/src/anthropic/lib/tools/agent_toolset.py +++ b/src/anthropic/lib/tools/agent_toolset.py @@ -274,6 +274,10 @@ class AgentToolContext: max_file_bytes: int | None | NotGiven = not_given max_image_base64_bytes: int | None | NotGiven = not_given max_pdf_bytes: int | None | NotGiven = not_given + # Whether to keep skill directories on disk after the context exits. + # When True, ``_cleanup_skills`` is a no-op, allowing the workdir to + # serve as a persistent skills cache across sessions. + keep_skills: bool = False # Resolved directories the write and edit tools refuse to modify. The # worker sets this to the roots of read-only memory stores so the agent # sees the error at write time instead of the change silently never @@ -316,6 +320,8 @@ async def _cleanup_skills(self) -> None: Only the directories this context created are removed — a pre-existing ``{workdir}/skills`` tree is left untouched. """ + if self.keep_skills: + return for skill_dir in self._skill_dirs: try: # ``shutil.rmtree`` is blocking; keep it off the event loop.