Skip to content

Skill source directory unavailable on Windows #2991

@onmyraedar

Description

@onmyraedar

Please read this first

  • Have you read the docs?Agents SDK docs
  • Have you searched for related issues? Others may have faced similar issues.

Describe the bug

I'm using the Agents SDK with a Modal sandbox. When I try to use a local skills directory, I'm getting the following error (running on Windows):

  File "C:\Users\Rae\Desktop\Code\.venv\Lib\site-packages\agents\sandbox\capabilities\skills.py", line 176, in load_skill
    raise SkillsConfigError(
agents.sandbox.errors.SkillsConfigError: lazy skill source directory is unavailable

The above exception was the direct cause of the following exception:

  File "C:\Users\Rae\Desktop\Code\.venv\Lib\site-packages\agents\run_internal\tool_execution.py", line 1577, in _run_single_tool
    raise UserError(f"Error running tool {func_tool.name}: {e}") from e
agents.exceptions.UserError: Error running tool load_skill: lazy skill source directory is unavailable

Debug information

  • Agents SDK version: (e.g. v0.0.3): 0.14.3 (specifically the main branch - I had opened an issue a few days ago and the fix is on main)
  • Python version (e.g. Python 3.14): 3.12.1
  • OpenAI version: 2.32.0

Repro steps

I have attached a script below. Underneath the script is an explanation of what happens.

import argparse
import asyncio
import json
import traceback
from pathlib import Path, PurePosixPath
from typing import Any

import modal
from dotenv import load_dotenv
from agents import Runner
from agents.extensions.sandbox.modal import (
    ModalImageSelector,
    ModalSandboxClient,
    ModalSandboxClientOptions,
)
from agents.run import RunConfig
from agents.sandbox import Manifest, SandboxAgent, SandboxRunConfig
from agents.sandbox.capabilities import (
    Compaction,
    Filesystem,
    LocalDirLazySkillSource,
    Shell,
    Skills,
)
from agents.sandbox.entries import Dir
from agents.sandbox.entries.artifacts import LocalDir

_REPO_ROOT = Path(__file__).resolve().parent
load_dotenv(_REPO_ROOT / ".env")
_LOCAL_SKILLS_DIR = _REPO_ROOT / "skills"
# Path inside the remote Modal function container.
_REMOTE_FUNCTION_SKILLS_DIR = PurePosixPath("/skills")
# Path inside the nested sandbox session.
_SANDBOX_SKILLS_DIR = PurePosixPath("/skills")
_SKILLS_SOURCE_PATH = Path(str(_SANDBOX_SKILLS_DIR))
_MODAL_APP_NAME = "cli-wrapper-test"

local_sandbox_base_image = modal.Image.debian_slim(python_version="3.12").add_local_dir(
    _LOCAL_SKILLS_DIR, remote_path=str(_SANDBOX_SKILLS_DIR), copy=True
)
remote_sandbox_base_image = modal.Image.debian_slim(
    python_version="3.12"
).add_local_dir(
    str(_REMOTE_FUNCTION_SKILLS_DIR),
    remote_path=str(_SANDBOX_SKILLS_DIR),
    copy=True,
)

app = modal.App(_MODAL_APP_NAME)

agent = SandboxAgent(
    name="workflow-session-start-tester",
    model="gpt-5.2",
    instructions="You are a precise sandbox test assistant.",
    capabilities=[
        Filesystem(),
        Shell(),
        Compaction(),
        Skills(
            lazy_from=LocalDirLazySkillSource(source=LocalDir(src=_SKILLS_SOURCE_PATH))
        ),
    ],
)

PROMPT = (
    "Load the workflow-session-start skill, then briefly confirm that it was loaded "
    "and what first action it recommends."
)


async def _get_skills_dir_check(sandbox_id: str) -> dict[str, Any]:
    sandbox = await modal.Sandbox.from_id.aio(sandbox_id)
    proc = await sandbox.exec.aio(
        "python3",
        "-c",
        (
            "import json, pathlib;"
            f"p = pathlib.Path('{_SANDBOX_SKILLS_DIR}');"
            "exists = p.is_dir();"
            "count = sum(1 for _ in p.iterdir()) if exists else 0;"
            "print(json.dumps({'exists': exists, 'contents_count': count}))"
        ),
    )

    stdout_chunks: list[str] = []
    async for chunk in proc.stdout:
        stdout_chunks.append(chunk)

    stderr_chunks: list[str] = []
    async for chunk in proc.stderr:
        stderr_chunks.append(chunk)

    return_code = await proc.wait.aio()
    if return_code != 0:
        stderr_text = "".join(stderr_chunks).strip()
        raise RuntimeError(
            f"find command failed with code {return_code}: {stderr_text or 'no stderr output'}"
        )

    stdout_text = "".join(stdout_chunks).strip()
    if not stdout_text:
        raise RuntimeError("No output while checking /skills directory.")
    try:
        return json.loads(stdout_text)
    except json.JSONDecodeError as exc:
        raise RuntimeError(
            f"Failed to parse /skills check output: {stdout_text}"
        ) from exc


async def _run_once(sandbox_image: modal.Image) -> dict[str, Any]:
    client = ModalSandboxClient(image=ModalImageSelector.from_image(sandbox_image))
    opts = ModalSandboxClientOptions(app_name=_MODAL_APP_NAME, timeout=15 * 60)
    session = await client.create(
        options=opts, manifest=Manifest(entries={"sessions": Dir()})
    )

    sandbox_id: str | None = None
    final_output: str | None = None
    run_error: str | None = None
    skills_check: dict[str, Any] | None = None
    skills_check_error: str | None = None

    try:
        await session.start()
        sandbox_id = getattr(session.state, "sandbox_id", None)

        try:
            result = await Runner.run(
                agent,
                PROMPT,
                run_config=RunConfig(sandbox=SandboxRunConfig(session=session)),
            )
            final_output = result.final_output
        except Exception:
            run_error = traceback.format_exc()

        if sandbox_id:
            try:
                skills_check = await _get_skills_dir_check(sandbox_id)
            except Exception as exc:
                skills_check_error = str(exc)
        else:
            skills_check_error = "Session does not expose sandbox_id."
    finally:
        await session.stop()
        await client.delete(session)

    return {
        "skills_source_path": str(_SKILLS_SOURCE_PATH),
        "final_output": final_output,
        "run_error": run_error,
        "sandbox_id": sandbox_id,
        "skills_check": skills_check,
        "skills_check_error": skills_check_error,
    }


app_image = (
    modal.Image.debian_slim(python_version="3.12")
    .uv_pip_install("openai-agents[modal]")
    .add_local_dir(
        _LOCAL_SKILLS_DIR, remote_path=str(_REMOTE_FUNCTION_SKILLS_DIR), copy=True
    )
)


@app.function(
    image=app_image,
    secrets=[modal.Secret.from_name("openai-api-key")],
)
async def run_remote() -> dict[str, Any]:
    return await _run_once(remote_sandbox_base_image)


def _print_result(payload: dict[str, Any]) -> None:
    # print(f"sandbox_id: {payload.get('sandbox_id')}")
    print("\n--- Agent output ---")
    print(payload.get("final_output") or "(no output; run failed)")
    if payload.get("run_error"):
        print("\n--- Run error ---")
        print(payload["run_error"])
    print(f"skills_source_path: {payload.get('skills_source_path')}")
    print("\n--- Skills directory check ---")
    check = payload.get("skills_check")
    if check:
        print(f"exists: {check.get('exists')}")
        print(f"contents_count: {check.get('contents_count')}")
    else:
        print("exists: unknown")
        print("contents_count: unknown")
    if payload.get("skills_check_error"):
        print("\n--- Skills check error ---")
        print(payload["skills_check_error"])


def main() -> None:
    remote = False

    payload: dict[str, Any]
    with app.run():
        if remote:
            payload = run_remote.remote()
        else:
            payload = asyncio.run(_run_once(local_sandbox_base_image))

    _print_result(payload)
    if payload.get("run_error"):
        raise SystemExit(1)


if __name__ == "__main__":
    main()

If you set remote=False on Windows, you can see from the output ("Skills directory check") that the skills are in the sandbox, but that there is still an error.

--- Agent output ---
(no output; run failed)

--- Run error (truncated) ---
  File "C:\Users\Rae\Desktop\Code\.venv\Lib\site-packages\agents\sandbox\capabilities\skills.py", line 176, in load_skill
    raise SkillsConfigError(
agents.sandbox.errors.SkillsConfigError: lazy skill source directory is unavailable

The above exception was the direct cause of the following exception:

  File "C:\Users\Rae\Desktop\Code\.venv\Lib\site-packages\agents\run_internal\tool_execution.py", line 1577, in _run_single_tool
    raise UserError(f"Error running tool {func_tool.name}: {e}") from e
agents.exceptions.UserError: Error running tool load_skill: lazy skill source directory is unavailable

skills_source_path: \skills

--- Skills directory check ---
exists: True
contents_count: 65

If you set remote=False (which runs this on a Linux machine - Modal function), you can see that the skills are in the sandbox and the code works - there is no error:

--- Agent output ---
Loaded `workflow-session-start` from `.agents/workflow-session-start/SKILL.md`.

First recommended action: check for prior context by looking for a relevant `chat_summary.md` under `sessions/` (and, if found, open by referencing what you did last time and asking whether to continue or start new).

skills_source_path: /skills

--- Skills directory check ---
exists: True
contents_count: 65

There is a difference in the skills_source_path printout (\skills on Windows vs /skills on Linux) that make me think it might be path-related, but I'm not sure.

I tried:

  • Setting the path as a string directly. This yielded the same error as described above.
agent = SandboxAgent(
    name="workflow-session-start-tester",
    model="gpt-5.2",
    instructions="You are a precise sandbox test assistant.",
    capabilities=[
        Filesystem(),
        Shell(),
        Compaction(),
        Skills(lazy_from=LocalDirLazySkillSource(source=LocalDir(src="/skills"))),
    ],
)
  • Setting a PurePosixPath. This raised a Pydantic error.
pydantic_core._pydantic_core.ValidationError: 1 validation error for LocalDir
src
  Input is not a valid path for <class 'pathlib.Path'> [type=path_type, input_value=PurePosixPath('/skills'), input_type=PurePosixPath]

Expected behavior

I expected the skills directory to work.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions