Source code for atloop.tools.filesystem.read_skill_file
"""Read skill file tool for accessing files from skill directories (local machine, not sandbox)."""
from pathlib import Path
from typing import Any, Dict, Optional
from atloop.tools.base import BaseTool, ToolResult
[docs]
class ReadSkillFileTool(BaseTool):
"""
Tool for reading files from skill directories (on local machine, not from sandbox workspace).
**🚨🚨🚨 CRITICAL**: This tool reads from skill directories on the LOCAL machine, NOT from the sandbox!
**Key Points:**
- **Skill files are stored LOCALLY** (on the host machine, in ~/.atloop/skills/ or project .atloop/skills/)
- **Workspace is a REMOTE SANDBOX** (/workspace) - it does NOT contain skill files or templates
- **When a skill mentions other files** (e.g., "see docx-js.md", "reference guide.md"), those files are LOCAL
- **You MUST use `read_skill_file`** to read skill-related files - they are NOT in the sandbox!
- **DO NOT try to use `read_file` or `run("cat ...")`** to find skill files in the sandbox - they don't exist there!
**Use cases:**
- Reading skill files (when skill mentions other files to reference)
- Reading files referenced in skill documentation
- Accessing skill-specific resources (templates, guides, examples)
**Path resolution:**
- With skill_name: Path is relative to skill directory (most common use case)
- Without skill_name: Absolute path or relative to ~/.atloop/
- Supports ~ expansion for home directory
**Important distinction:**
- ✅ **Skill files** → Use `read_skill_file` (stored locally)
- ✅ **Workspace files** → Use `read_file` (stored in remote sandbox /workspace)
- ❌ **Never use `read_file` or `run` to find skill files** - they are not in the sandbox!
"""
[docs]
def __init__(self, skill_loader=None):
"""
Initialize read skill file tool.
Args:
skill_loader: Optional skill loader instance for resolving skill paths
"""
self.skill_loader = skill_loader
@property
def name(self) -> str:
"""Tool name."""
return "read_skill_file"
@property
def description(self) -> str:
"""Tool description."""
return "从技能目录读取文件(⚠️ 技能文件存储在本地机器,不在远程沙盒中。当 skill 中提到其他文件时,必须使用此工具读取,不能使用 read_file 或 run 命令在沙盒中查找)"
[docs]
def validate_args(self, args: Dict[str, Any]) -> tuple[bool, Optional[str]]:
"""Validate arguments."""
if "path" not in args:
return False, "Missing required argument: 'path'"
if not isinstance(args["path"], str):
return False, "Argument 'path' must be a string"
if "offset" in args and not isinstance(args.get("offset"), int):
return False, "Argument 'offset' must be an integer"
if "limit" in args and not isinstance(args.get("limit"), int):
return False, "Argument 'limit' must be an integer"
if "skill_name" in args and not isinstance(args.get("skill_name"), str):
return False, "Argument 'skill_name' must be a string"
return True, None
[docs]
def execute(self, args: Dict[str, Any]) -> ToolResult:
"""
Execute read skill file tool.
**🚨🚨🚨 CRITICAL**: This tool reads from skill directories on the LOCAL machine, NOT from the sandbox workspace!
**Important understanding:**
- **Skill files are stored LOCALLY** (on the host machine)
- **Workspace is a REMOTE SANDBOX** (/workspace) - it does NOT contain skill files
- **When a skill mentions other files**, those files are LOCAL, not in the sandbox
- **You MUST use `read_skill_file`** to read skill-related files
- **DO NOT try to use `read_file` or `run("cat ...")`** to find skill files in the sandbox - they don't exist there!
Use `read_file` to read files from the sandbox workspace (/workspace).
**Args:**
args: Tool arguments dictionary
- path (str, required): File path. Resolution depends on skill_name:
- If skill_name provided: Path is relative to skill directory
- If path starts with ~: Expanded to home directory
- If absolute path: Used as-is
- Otherwise: Relative to ~/.atloop/
- skill_name (str, optional): Skill name. If provided, path is resolved
relative to the skill's directory. Use this when a skill mentions
other files to reference.
- offset (int, optional): Start line number (1-indexed). Default: 1.
Use for reading large files in chunks.
- limit (int, optional): Number of lines to read. If not specified,
reads from offset to end of file.
**Returns:**
ToolResult with:
- ok (bool): True if file was read successfully (no errors in stderr)
- stdout (str): File content (or metadata for binary/large files)
- stderr (str): Error messages if any (empty string means success)
- meta (dict): Contains path, file_size, start_line, end_line
**Examples:**
# Read skill file (when skill mentions other files)
read_skill_file(path="references/guide.md", skill_name="long_document_writer")
# Read skill documentation
read_skill_file(path="docx-js.md", skill_name="docx")
# Read with line range
read_skill_file(path="skill-doc.md", skill_name="docx", offset=1, limit=50)
**When to use this vs read_file:**
- ✅ Reading skill files → use `read_skill_file` (stored locally, NOT in sandbox)
- ✅ Reading files referenced in skills → use `read_skill_file` (skill mentions are LOCAL files)
- ✅ Reading skill templates/examples → use `read_skill_file` (stored locally)
- ❌ Reading workspace files → use `read_file` (sandbox files in /workspace)
- ❌ **DO NOT use `read_file` or `run` to find skill files** - they are NOT in the sandbox!
**File Size Limits:**
- Files larger than 10MB return metadata only
- Use offset/limit to read specific sections of large files
**Error Handling:**
- Success is determined by stderr content, not exit code
- If stderr is empty, the operation succeeded
- Check stderr for specific error messages if ok=False
"""
path_str = args["path"]
skill_name = args.get("skill_name")
offset = args.get("offset")
limit = args.get("limit")
# Resolve file path
if skill_name and self.skill_loader:
# Resolve relative to skill directory
skill = self.skill_loader.skills.get(skill_name)
if not skill:
return ToolResult(
ok=False,
stdout="",
stderr=f"Skill '{skill_name}' not found. Cannot resolve path relative to skill.",
meta={"path": path_str, "skill_name": skill_name},
)
skill_dir = skill["dir"]
file_path = skill_dir / path_str
elif path_str.startswith("~"):
# Expand ~ to home directory
file_path = Path(path_str).expanduser()
elif Path(path_str).is_absolute():
# Absolute path
file_path = Path(path_str)
else:
# Relative to ~/.atloop/ (common case for skill resources)
file_path = Path.home() / ".atloop" / path_str
# Check if file exists
if not file_path.exists():
return ToolResult(
ok=False,
stdout="",
stderr=f"File not found: {file_path}. "
f"(Resolved from: {path_str}, skill: {skill_name or 'none'})",
meta={"path": str(file_path), "original_path": path_str, "skill_name": skill_name},
)
if not file_path.is_file():
return ToolResult(
ok=False,
stdout="",
stderr=f"Path is not a file: {file_path}",
meta={"path": str(file_path)},
)
# Check file size
file_size = file_path.stat().st_size
max_file_size = 10 * 1024 * 1024 # 10MB
if file_size > max_file_size:
return ToolResult(
ok=True,
stdout=f"[File: {file_path}]\nSize: {file_size} bytes\n\nFile is too large to display (>10MB). Use offset and limit to read specific lines.",
stderr="",
meta={"path": str(file_path), "file_size": file_size},
)
# Read file content
try:
# Try to read as text first
content = file_path.read_text(encoding="utf-8")
except UnicodeDecodeError:
# Binary file
return ToolResult(
ok=True,
stdout=f"[File: {file_path}]\nType: binary\nSize: {file_size} bytes\n\nThis file is binary and cannot be displayed as text.",
stderr="",
meta={"path": str(file_path), "file_type": "binary", "file_size": file_size},
)
except Exception as e:
return ToolResult(
ok=False,
stdout="",
stderr=f"Error reading file: {e}",
meta={"path": str(file_path), "error": str(e)},
)
# Handle line range if specified
if offset is not None or limit is not None:
lines = content.split("\n")
start_line = (offset - 1) if offset is not None else 0
end_line = (start_line + limit) if limit is not None else len(lines)
# Validate range
if start_line < 0:
start_line = 0
if end_line > len(lines):
end_line = len(lines)
if start_line >= len(lines):
return ToolResult(
ok=True,
stdout="",
stderr="",
meta={
"path": str(file_path),
"start_line": offset,
"end_line": end_line,
"total_lines": len(lines),
},
)
selected_lines = lines[start_line:end_line]
content = "\n".join(selected_lines)
return ToolResult(
ok=True,
stdout=content,
stderr="",
meta={
"path": str(file_path),
"file_size": file_size,
"start_line": offset,
"end_line": (offset + limit - 1) if (offset and limit) else None,
},
)