Skip to content

ant_ai.tools.builtins.filesystem_tool

FilesystemTool pydantic-model

Bases: Tool

Tools for reading and writing files within the agent workspace.

Example
from ant_ai.agent import Agent
from ant_ai.tools.builtins.filesystem_tool import FilesystemTool

fs = FilesystemTool(workspace_root="/workspace")
agent = Agent(tools=[fs], ...)

The agent can now call:

- FilesystemTool_read_file(path="notes.txt")
- FilesystemTool_write_file(path="output.txt", content="hello")
- FilesystemTool_list_dir(path="src/")
- FilesystemTool_search(pattern="TODO", path="src/")
Notes

All paths are resolved relative to workspace_root and sandboxed within it. Failures -- a missing file, an invalid regex, an attempt to escape the workspace (e.g. "../etc/passwd", "/absolute/path") -- raise ToolError, which ToolStep turns into a recoverable ERROR: ... message for the agent and marks as an error.

Show JSON schema:
{
  "description": "Tools for reading and writing files within the agent workspace.\n\nExample:\n    ```python\n    from ant_ai.agent import Agent\n    from ant_ai.tools.builtins.filesystem_tool import FilesystemTool\n\n    fs = FilesystemTool(workspace_root=\"/workspace\")\n    agent = Agent(tools=[fs], ...)\n    ```\n\n    The agent can now call:\n\n        - FilesystemTool_read_file(path=\"notes.txt\")\n        - FilesystemTool_write_file(path=\"output.txt\", content=\"hello\")\n        - FilesystemTool_list_dir(path=\"src/\")\n        - FilesystemTool_search(pattern=\"TODO\", path=\"src/\")\n\nNotes:\n    All paths are resolved relative to `workspace_root` and sandboxed within it.\n    Failures -- a missing file, an invalid regex, an attempt to escape the\n    workspace (e.g. \"../etc/passwd\", \"/absolute/path\") -- raise `ToolError`,\n    which `ToolStep` turns into a recoverable `ERROR: ...` message for the\n    agent and marks as an error.",
  "properties": {
    "name": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Tool name.",
      "title": "Name"
    },
    "description": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Tool description. Is used by the LLM to decide whether to call or not the specific tool.",
      "title": "Description"
    },
    "parameters": {
      "anyOf": [
        {
          "additionalProperties": true,
          "type": "object"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "The parameters needed by the tool. This is a self-constructed field.",
      "title": "Parameters"
    },
    "workspace_root": {
      "default": ".",
      "format": "path",
      "title": "Workspace Root",
      "type": "string"
    }
  },
  "title": "FilesystemTool",
  "type": "object"
}

Fields:

  • name (str | None)
  • description (str | None)
  • parameters (dict[str, Any] | None)
  • __namespace_methods__ (list[str])
  • workspace_root (Path)
  • _ignore_spec (PathSpec | None)

Validators:

  • _set_defaults
Source code in src/ant_ai/tools/builtins/filesystem_tool.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
class FilesystemTool(Tool):
    """
    Tools for reading and writing files within the agent workspace.

    Example:
        ```python
        from ant_ai.agent import Agent
        from ant_ai.tools.builtins.filesystem_tool import FilesystemTool

        fs = FilesystemTool(workspace_root="/workspace")
        agent = Agent(tools=[fs], ...)
        ```

        The agent can now call:

            - FilesystemTool_read_file(path="notes.txt")
            - FilesystemTool_write_file(path="output.txt", content="hello")
            - FilesystemTool_list_dir(path="src/")
            - FilesystemTool_search(pattern="TODO", path="src/")

    Notes:
        All paths are resolved relative to `workspace_root` and sandboxed within it.
        Failures -- a missing file, an invalid regex, an attempt to escape the
        workspace (e.g. "../etc/passwd", "/absolute/path") -- raise `ToolError`,
        which `ToolStep` turns into a recoverable `ERROR: ...` message for the
        agent and marks as an error.
    """

    workspace_root: Path = Path()
    _ignore_spec: pathspec.PathSpec | None = PrivateAttr(default=None)

    def __init__(self, workspace_root: str = "/workspace", **data):
        super().__init__(**data)
        self.workspace_root = Path(workspace_root).resolve()
        self._ignore_spec = self._load_ignore_spec()

    def _load_ignore_spec(self) -> pathspec.PathSpec | None:
        gitignore: Path = self.workspace_root / ".gitignore"
        if not gitignore.is_file():
            return None
        lines: list[str] = gitignore.read_text(encoding="utf-8").splitlines()
        return pathspec.PathSpec.from_lines("gitwildmatch", lines)

    def _is_ignored(self, entry: Path, is_dir: bool) -> bool:
        if self._ignore_spec is None:
            return False
        rel: str = entry.relative_to(self.workspace_root).as_posix()
        return self._ignore_spec.match_file(f"{rel}/" if is_dir else rel)

    def read_file(self, path: str) -> str:
        """Read the full contents of a file. Path is relative to /workspace."""
        try:
            return self._resolve(path).read_text(encoding="utf-8")
        except ValueError as e:
            raise ToolError(str(e)) from e
        except FileNotFoundError as e:
            raise ToolError(f"file not found: {path}") from e
        except OSError as e:
            raise ToolError(f"{e.strerror or e}: {path}") from e

    def write_file(self, path: str, content: str) -> str:
        """Write content to a file, overwriting if it exists. Creates parent directories as needed. Path is relative to /workspace."""
        try:
            target: Path = self._resolve(path)
            target.parent.mkdir(parents=True, exist_ok=True)
            target.write_text(content, encoding="utf-8")
            return f"Written {len(content.encode('utf-8'))} bytes to {path}"
        except ValueError as e:
            raise ToolError(str(e)) from e
        except OSError as e:
            raise ToolError(f"{e.strerror or e}: {path}") from e

    def list_dir(self, path: str = ".", depth: int = 4) -> list[str]:
        """List files and directories recursively at the given path, up to `depth` levels deep. Path is relative to /workspace. Defaults to workspace root."""
        try:
            target: Path = self._resolve(path)
            results: list[str] = []
            self._walk(target, target, depth, results)
            return sorted(results)
        except ValueError as e:
            raise ToolError(str(e)) from e
        except FileNotFoundError as e:
            raise ToolError(f"directory not found: {path}") from e
        except OSError as e:
            raise ToolError(f"{e.strerror or e}: {path}") from e

    def _walk(self, base: Path, current: Path, depth: int, results: list[str]) -> None:
        """Collect paths (relative to `base`) of entries under `current`, recursing up to `depth` levels."""
        for entry in current.iterdir():
            is_dir = entry.is_dir()
            if self._is_ignored(entry, is_dir):
                continue
            rel = str(entry.relative_to(base))
            results.append(f"{rel}/" if is_dir else rel)
            if is_dir and depth > 0:
                self._walk(base, entry, depth - 1, results)

    def search(self, pattern: str, path: str = ".", depth: int = 4) -> str:
        """Search for a regex pattern recursively within path, up to `depth` levels deep. Path is relative to /workspace. Returns matching lines in the format 'file:lineno: content'."""
        try:
            root: Path = self._resolve(path)
            regex: Pattern[str] = re.compile(pattern)
            results: list[str] = []
            for dirpath, dirnames, filenames in os.walk(root, followlinks=False):
                dirnames.sort()
                dirnames[:] = [
                    d for d in dirnames if not self._is_ignored(Path(dirpath, d), True)
                ]
                if len(Path(dirpath).relative_to(root).parts) >= depth:
                    dirnames.clear()
                for filename in sorted(filenames):
                    file: Path = Path(dirpath) / filename
                    if not file.is_file() or self._is_ignored(file, False):
                        continue
                    try:
                        for lineno, line in enumerate(
                            file.read_text(
                                encoding="utf-8", errors="replace"
                            ).splitlines(),
                            start=1,
                        ):
                            if regex.search(line):
                                rel: Path = file.relative_to(self.workspace_root)
                                results.append(f"{rel}:{lineno}: {line}")
                    except Exception:
                        continue
            return "\n".join(results) if results else "No matches found"
        except ValueError as e:
            raise ToolError(str(e)) from e
        except re.error as e:
            raise ToolError(f"invalid regex pattern: {e}") from e

    def _resolve(self, path: str) -> Path:
        """Resolve path relative to base, ensuring it stays within base."""
        resolved: Path = (self.workspace_root / path).resolve()
        if not resolved.is_relative_to(self.workspace_root):
            raise ValueError(f"Path '{path}' escapes the workspace boundary")
        return resolved

read_file

read_file(path: str) -> str

Read the full contents of a file. Path is relative to /workspace.

Source code in src/ant_ai/tools/builtins/filesystem_tool.py
63
64
65
66
67
68
69
70
71
72
def read_file(self, path: str) -> str:
    """Read the full contents of a file. Path is relative to /workspace."""
    try:
        return self._resolve(path).read_text(encoding="utf-8")
    except ValueError as e:
        raise ToolError(str(e)) from e
    except FileNotFoundError as e:
        raise ToolError(f"file not found: {path}") from e
    except OSError as e:
        raise ToolError(f"{e.strerror or e}: {path}") from e

write_file

write_file(path: str, content: str) -> str

Write content to a file, overwriting if it exists. Creates parent directories as needed. Path is relative to /workspace.

Source code in src/ant_ai/tools/builtins/filesystem_tool.py
74
75
76
77
78
79
80
81
82
83
84
def write_file(self, path: str, content: str) -> str:
    """Write content to a file, overwriting if it exists. Creates parent directories as needed. Path is relative to /workspace."""
    try:
        target: Path = self._resolve(path)
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(content, encoding="utf-8")
        return f"Written {len(content.encode('utf-8'))} bytes to {path}"
    except ValueError as e:
        raise ToolError(str(e)) from e
    except OSError as e:
        raise ToolError(f"{e.strerror or e}: {path}") from e

list_dir

list_dir(path: str = '.', depth: int = 4) -> list[str]

List files and directories recursively at the given path, up to depth levels deep. Path is relative to /workspace. Defaults to workspace root.

Source code in src/ant_ai/tools/builtins/filesystem_tool.py
86
87
88
89
90
91
92
93
94
95
96
97
98
def list_dir(self, path: str = ".", depth: int = 4) -> list[str]:
    """List files and directories recursively at the given path, up to `depth` levels deep. Path is relative to /workspace. Defaults to workspace root."""
    try:
        target: Path = self._resolve(path)
        results: list[str] = []
        self._walk(target, target, depth, results)
        return sorted(results)
    except ValueError as e:
        raise ToolError(str(e)) from e
    except FileNotFoundError as e:
        raise ToolError(f"directory not found: {path}") from e
    except OSError as e:
        raise ToolError(f"{e.strerror or e}: {path}") from e

search

search(
    pattern: str, path: str = ".", depth: int = 4
) -> str

Search for a regex pattern recursively within path, up to depth levels deep. Path is relative to /workspace. Returns matching lines in the format 'file:lineno: content'.

Source code in src/ant_ai/tools/builtins/filesystem_tool.py
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def search(self, pattern: str, path: str = ".", depth: int = 4) -> str:
    """Search for a regex pattern recursively within path, up to `depth` levels deep. Path is relative to /workspace. Returns matching lines in the format 'file:lineno: content'."""
    try:
        root: Path = self._resolve(path)
        regex: Pattern[str] = re.compile(pattern)
        results: list[str] = []
        for dirpath, dirnames, filenames in os.walk(root, followlinks=False):
            dirnames.sort()
            dirnames[:] = [
                d for d in dirnames if not self._is_ignored(Path(dirpath, d), True)
            ]
            if len(Path(dirpath).relative_to(root).parts) >= depth:
                dirnames.clear()
            for filename in sorted(filenames):
                file: Path = Path(dirpath) / filename
                if not file.is_file() or self._is_ignored(file, False):
                    continue
                try:
                    for lineno, line in enumerate(
                        file.read_text(
                            encoding="utf-8", errors="replace"
                        ).splitlines(),
                        start=1,
                    ):
                        if regex.search(line):
                            rel: Path = file.relative_to(self.workspace_root)
                            results.append(f"{rel}:{lineno}: {line}")
                except Exception:
                    continue
        return "\n".join(results) if results else "No matches found"
    except ValueError as e:
        raise ToolError(str(e)) from e
    except re.error as e:
        raise ToolError(f"invalid regex pattern: {e}") from e