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.
Please read this first
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):
Debug information
v0.0.3): 0.14.3 (specifically themainbranch - I had opened an issue a few days ago and the fix is on main)Repro steps
I have attached a script below. Underneath the script is an explanation of what happens.
If you set
remote=Falseon Windows, you can see from the output ("Skills directory check") that the skills are in the sandbox, but that there is still an error.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:There is a difference in the skills_source_path printout (
\skillson Windows vs/skillson Linux) that make me think it might be path-related, but I'm not sure.I tried:
PurePosixPath. This raised a Pydantic error.Expected behavior
I expected the skills directory to work.