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. Attempts to escape the workspace (e.g. "../etc/passwd", "/absolute/path") are caught by _resolve and returned to the caller as an error string instead of raising — so the agent receives a recoverable error message rather than an exception.

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    Attempts to escape the workspace (e.g. \"../etc/passwd\", \"/absolute/path\")\n    are caught by `_resolve` and returned to the caller as an error string\n    instead of raising \u2014 so the agent receives a recoverable error message\n    rather than an exception.",
  "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
152
153
154
155
156
157
158
159
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.
        Attempts to escape the workspace (e.g. "../etc/passwd", "/absolute/path")
        are caught by `_resolve` and returned to the caller as an error string
        instead of raising — so the agent receives a recoverable error message
        rather than an exception.
    """

    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:
            return f"Error: {e}"
        except FileNotFoundError:
            return f"Error: file not found: {path}"
        except OSError as e:
            return f"Error: {e.strerror or e}: {path}"
        except Exception as e:
            return f"Error: {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:
            return f"Error: {e}"
        except OSError as e:
            return f"Error: {e.strerror or e}: {path}"
        except Exception as e:
            return f"Error: {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:
            return [f"Error: {e}"]
        except FileNotFoundError:
            return [f"Error: directory not found: {path}"]
        except OSError as e:
            return [f"Error: {e.strerror or e}: {path}"]
        except Exception as e:
            return [f"Error: {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:
            return f"Error: {e}"
        except re.error as e:
            return f"Error: invalid regex pattern: {e}"
        except Exception as e:
            return f"Error: {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
73
74
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:
        return f"Error: {e}"
    except FileNotFoundError:
        return f"Error: file not found: {path}"
    except OSError as e:
        return f"Error: {e.strerror or e}: {path}"
    except Exception as e:
        return f"Error: {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
76
77
78
79
80
81
82
83
84
85
86
87
88
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:
        return f"Error: {e}"
    except OSError as e:
        return f"Error: {e.strerror or e}: {path}"
    except Exception as e:
        return f"Error: {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
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
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:
        return [f"Error: {e}"]
    except FileNotFoundError:
        return [f"Error: directory not found: {path}"]
    except OSError as e:
        return [f"Error: {e.strerror or e}: {path}"]
    except Exception as e:
        return [f"Error: {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
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
152
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:
        return f"Error: {e}"
    except re.error as e:
        return f"Error: invalid regex pattern: {e}"
    except Exception as e:
        return f"Error: {e}"