Skip to content

Tools

pycodeloop.core.tools.filesystem.ReadFileTool

Bases: Tool

Source code in pycodeloop/core/tools/filesystem.py
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
class ReadFileTool(Tool):
    name = "read_file"
    description = "Read a text file's contents, optionally a line range."
    parameters = {
        "type": "object",
        "properties": {
            "path": {"type": "string", "description": "File path to read"},
            "offset": {
                "type": "integer",
                "description": "1-indexed start line",
            },
            "limit": {"type": "integer", "description": "Max lines to read"},
        },
        "required": ["path"],
    }

    def run(self, path: str, offset: int = 1, limit: int | None = None) -> ToolResult:
        target = Path(path)

        try:
            lines = target.read_text().splitlines()
        except OSError as exc:
            return ToolResult(output=f"Error reading {path}: {exc}", is_error=True)

        start = max(offset - 1, 0)
        end = start + limit if limit else len(lines)
        numbered = [
            f"{i + start + 1}\t{line}" for i, line in enumerate(lines[start:end])
        ]

        return ToolResult(output=truncate("\n".join(numbered)))

description = "Read a text file's contents, optionally a line range." class-attribute instance-attribute

name = 'read_file' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {'path': {'type': 'string', 'description': 'File path to read'}, 'offset': {'type': 'integer', 'description': '1-indexed start line'}, 'limit': {'type': 'integer', 'description': 'Max lines to read'}}, 'required': ['path']} class-attribute instance-attribute

run(path, offset=1, limit=None)

Source code in pycodeloop/core/tools/filesystem.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def run(self, path: str, offset: int = 1, limit: int | None = None) -> ToolResult:
    target = Path(path)

    try:
        lines = target.read_text().splitlines()
    except OSError as exc:
        return ToolResult(output=f"Error reading {path}: {exc}", is_error=True)

    start = max(offset - 1, 0)
    end = start + limit if limit else len(lines)
    numbered = [
        f"{i + start + 1}\t{line}" for i, line in enumerate(lines[start:end])
    ]

    return ToolResult(output=truncate("\n".join(numbered)))

pycodeloop.core.tools.filesystem.WriteFileTool

Bases: Tool

Source code in pycodeloop/core/tools/filesystem.py
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
class WriteFileTool(Tool):
    name = "write_file"
    description = "Write content to a file, creating or overwriting it."
    parameters = {
        "type": "object",
        "properties": {
            "path": {"type": "string"},
            "content": {"type": "string"},
        },
        "required": ["path", "content"],
    }
    dangerous = True

    def preview(self, path: str, content: str, **_) -> str:
        target = Path(path)

        try:
            before = target.read_text()
        except OSError:
            before = ""
        return _diff(path, before, content)

    def run(self, path: str, content: str) -> ToolResult:
        target = Path(path)

        try:
            target.parent.mkdir(parents=True, exist_ok=True)
            target.write_text(content)
        except OSError as exc:
            return ToolResult(output=f"Error writing {path}: {exc}", is_error=True)

        return ToolResult(output=f"Wrote {len(content)} bytes to {path}")

dangerous = True class-attribute instance-attribute

description = 'Write content to a file, creating or overwriting it.' class-attribute instance-attribute

name = 'write_file' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {'path': {'type': 'string'}, 'content': {'type': 'string'}}, 'required': ['path', 'content']} class-attribute instance-attribute

preview(path, content, **_)

Source code in pycodeloop/core/tools/filesystem.py
68
69
70
71
72
73
74
75
def preview(self, path: str, content: str, **_) -> str:
    target = Path(path)

    try:
        before = target.read_text()
    except OSError:
        before = ""
    return _diff(path, before, content)

run(path, content)

Source code in pycodeloop/core/tools/filesystem.py
77
78
79
80
81
82
83
84
85
86
def run(self, path: str, content: str) -> ToolResult:
    target = Path(path)

    try:
        target.parent.mkdir(parents=True, exist_ok=True)
        target.write_text(content)
    except OSError as exc:
        return ToolResult(output=f"Error writing {path}: {exc}", is_error=True)

    return ToolResult(output=f"Wrote {len(content)} bytes to {path}")

pycodeloop.core.tools.filesystem.EditFileTool

Bases: Tool

Source code in pycodeloop/core/tools/filesystem.py
 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
160
161
162
163
164
165
166
167
168
169
class EditFileTool(Tool):
    name = "edit_file"
    description = "Replace an exact substring in a file with a new one."
    parameters = {
        "type": "object",
        "properties": {
            "path": {"type": "string"},
            "old_string": {"type": "string"},
            "new_string": {"type": "string"},
            "replace_all": {"type": "boolean", "default": False},
        },
        "required": ["path", "old_string", "new_string"],
    }
    dangerous = True

    def _apply(
        self, path: str, old_string: str, new_string: str, replace_all: bool
    ) -> tuple[Path, str, str] | ToolResult:
        target = Path(path)

        try:
            text = target.read_text()
        except OSError as exc:
            return ToolResult(output=f"Error reading {path}: {exc}", is_error=True)

        count = text.count(old_string)

        if count == 0:
            return ToolResult(output=f"old_string not found in {path}", is_error=True)

        if count > 1 and not replace_all:
            return ToolResult(
                output=(
                    f"old_string is not unique in {path} "
                    f"({count} matches); pass replace_all=true or give "
                    "more context"
                ),
                is_error=True,
            )

        new_text = (
            text.replace(old_string, new_string)
            if replace_all
            else text.replace(old_string, new_string, 1)
        )

        return target, text, new_text

    def preview(
        self,
        path: str,
        old_string: str,
        new_string: str,
        replace_all: bool = False,
        **_,
    ) -> str:
        result = self._apply(path, old_string, new_string, replace_all)

        if isinstance(result, ToolResult):
            return result.output

        _target, before, after = result

        return _diff(path, before, after)

    def run(
        self,
        path: str,
        old_string: str,
        new_string: str,
        replace_all: bool = False,
    ) -> ToolResult:
        result = self._apply(path, old_string, new_string, replace_all)

        if isinstance(result, ToolResult):
            return result

        target, _before, after = result
        target.write_text(after)

        return ToolResult(output=f"Edited {path}")

dangerous = True class-attribute instance-attribute

description = 'Replace an exact substring in a file with a new one.' class-attribute instance-attribute

name = 'edit_file' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {'path': {'type': 'string'}, 'old_string': {'type': 'string'}, 'new_string': {'type': 'string'}, 'replace_all': {'type': 'boolean', 'default': False}}, 'required': ['path', 'old_string', 'new_string']} class-attribute instance-attribute

preview(path, old_string, new_string, replace_all=False, **_)

Source code in pycodeloop/core/tools/filesystem.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
def preview(
    self,
    path: str,
    old_string: str,
    new_string: str,
    replace_all: bool = False,
    **_,
) -> str:
    result = self._apply(path, old_string, new_string, replace_all)

    if isinstance(result, ToolResult):
        return result.output

    _target, before, after = result

    return _diff(path, before, after)

run(path, old_string, new_string, replace_all=False)

Source code in pycodeloop/core/tools/filesystem.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def run(
    self,
    path: str,
    old_string: str,
    new_string: str,
    replace_all: bool = False,
) -> ToolResult:
    result = self._apply(path, old_string, new_string, replace_all)

    if isinstance(result, ToolResult):
        return result

    target, _before, after = result
    target.write_text(after)

    return ToolResult(output=f"Edited {path}")

pycodeloop.core.tools.filesystem.DeleteFileTool

Bases: Tool

Source code in pycodeloop/core/tools/filesystem.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
class DeleteFileTool(Tool):
    name = "delete_file"
    description = "Delete a file."
    parameters = {
        "type": "object",
        "properties": {"path": {"type": "string"}},
        "required": ["path"],
    }
    dangerous = True

    def preview(self, path: str, **_) -> str:
        target = Path(path)

        try:
            before = target.read_text()
        except OSError as exc:
            return f"Error reading {path}: {exc}"
        return _diff(path, before, "")

    def run(self, path: str) -> ToolResult:
        target = Path(path)

        try:
            target.unlink()
        except OSError as exc:
            return ToolResult(output=f"Error deleting {path}: {exc}", is_error=True)

        return ToolResult(output=f"Deleted {path}")

dangerous = True class-attribute instance-attribute

description = 'Delete a file.' class-attribute instance-attribute

name = 'delete_file' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {'path': {'type': 'string'}}, 'required': ['path']} class-attribute instance-attribute

preview(path, **_)

Source code in pycodeloop/core/tools/filesystem.py
182
183
184
185
186
187
188
189
def preview(self, path: str, **_) -> str:
    target = Path(path)

    try:
        before = target.read_text()
    except OSError as exc:
        return f"Error reading {path}: {exc}"
    return _diff(path, before, "")

run(path)

Source code in pycodeloop/core/tools/filesystem.py
191
192
193
194
195
196
197
198
199
def run(self, path: str) -> ToolResult:
    target = Path(path)

    try:
        target.unlink()
    except OSError as exc:
        return ToolResult(output=f"Error deleting {path}: {exc}", is_error=True)

    return ToolResult(output=f"Deleted {path}")

pycodeloop.core.tools.filesystem.ListDirTool

Bases: Tool

Source code in pycodeloop/core/tools/filesystem.py
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
class ListDirTool(Tool):
    name = "list_dir"
    description = "List files and directories at a given path."
    parameters = {
        "type": "object",
        "properties": {"path": {"type": "string", "default": "."}},
    }

    def run(self, path: str = ".") -> ToolResult:
        target = Path(path)

        try:
            entries = sorted(target.iterdir())
        except OSError as exc:
            return ToolResult(output=f"Error listing {path}: {exc}", is_error=True)

        lines = [f"{'d' if e.is_dir() else 'f'} {e.name}" for e in entries]

        return ToolResult(output="\n".join(lines))

description = 'List files and directories at a given path.' class-attribute instance-attribute

name = 'list_dir' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {'path': {'type': 'string', 'default': '.'}}} class-attribute instance-attribute

run(path='.')

Source code in pycodeloop/core/tools/filesystem.py
210
211
212
213
214
215
216
217
218
219
220
def run(self, path: str = ".") -> ToolResult:
    target = Path(path)

    try:
        entries = sorted(target.iterdir())
    except OSError as exc:
        return ToolResult(output=f"Error listing {path}: {exc}", is_error=True)

    lines = [f"{'d' if e.is_dir() else 'f'} {e.name}" for e in entries]

    return ToolResult(output="\n".join(lines))

pycodeloop.core.tools.search.GlobTool

Bases: Tool

Source code in pycodeloop/core/tools/search.py
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
class GlobTool(Tool):
    name = "glob"
    description = "Find files by glob pattern (e.g. '**/*.py')."
    parameters = {
        "type": "object",
        "properties": {
            "pattern": {"type": "string"},
            "path": {"type": "string", "default": "."},
            "max_results": {"type": "integer", "default": 100},
        },
        "required": ["pattern"],
    }

    def run(self, pattern: str, path: str = ".", max_results: int = 100) -> ToolResult:
        try:
            matches = [
                str(p)
                for p in Path(path).glob(pattern)
                if not set(p.parts) & _SKIP_DIRS
            ]
        except (OSError, ValueError) as exc:
            return ToolResult(output=f"Invalid glob: {exc}", is_error=True)

        matches = sorted(matches)[:max_results]

        return ToolResult(output="\n".join(matches) if matches else "No matches.")

description = "Find files by glob pattern (e.g. '**/*.py')." class-attribute instance-attribute

name = 'glob' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {'pattern': {'type': 'string'}, 'path': {'type': 'string', 'default': '.'}, 'max_results': {'type': 'integer', 'default': 100}}, 'required': ['pattern']} class-attribute instance-attribute

run(pattern, path='.', max_results=100)

Source code in pycodeloop/core/tools/search.py
73
74
75
76
77
78
79
80
81
82
83
84
85
def run(self, pattern: str, path: str = ".", max_results: int = 100) -> ToolResult:
    try:
        matches = [
            str(p)
            for p in Path(path).glob(pattern)
            if not set(p.parts) & _SKIP_DIRS
        ]
    except (OSError, ValueError) as exc:
        return ToolResult(output=f"Invalid glob: {exc}", is_error=True)

    matches = sorted(matches)[:max_results]

    return ToolResult(output="\n".join(matches) if matches else "No matches.")

pycodeloop.core.tools.search.GrepTool

Bases: Tool

Source code in pycodeloop/core/tools/search.py
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
class GrepTool(Tool):
    name = "grep"
    description = "Search for a regex pattern across files under a path."
    parameters = {
        "type": "object",
        "properties": {
            "pattern": {"type": "string"},
            "path": {"type": "string", "default": "."},
            "max_results": {"type": "integer", "default": 100},
        },
        "required": ["pattern"],
    }

    def run(self, pattern: str, path: str = ".", max_results: int = 100) -> ToolResult:
        try:
            regex = re.compile(pattern)
        except re.error as exc:
            return ToolResult(output=f"Invalid regex: {exc}", is_error=True)

        matches: list[str] = []
        for file_path in Path(path).rglob("*"):
            if not file_path.is_file() or set(file_path.parts) & _SKIP_DIRS:
                continue
            try:
                text = file_path.read_text(errors="ignore")
            except OSError:
                continue
            for lineno, line in enumerate(text.splitlines(), start=1):
                if regex.search(line):
                    matches.append(f"{file_path}:{lineno}: {line.strip()}")
                    if len(matches) >= max_results:
                        return ToolResult(output=truncate("\n".join(matches)))

        return ToolResult(
            output=truncate("\n".join(matches)) if matches else "No matches."
        )

description = 'Search for a regex pattern across files under a path.' class-attribute instance-attribute

name = 'grep' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {'pattern': {'type': 'string'}, 'path': {'type': 'string', 'default': '.'}, 'max_results': {'type': 'integer', 'default': 100}}, 'required': ['pattern']} class-attribute instance-attribute

run(pattern, path='.', max_results=100)

Source code in pycodeloop/core/tools/search.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def run(self, pattern: str, path: str = ".", max_results: int = 100) -> ToolResult:
    try:
        regex = re.compile(pattern)
    except re.error as exc:
        return ToolResult(output=f"Invalid regex: {exc}", is_error=True)

    matches: list[str] = []
    for file_path in Path(path).rglob("*"):
        if not file_path.is_file() or set(file_path.parts) & _SKIP_DIRS:
            continue
        try:
            text = file_path.read_text(errors="ignore")
        except OSError:
            continue
        for lineno, line in enumerate(text.splitlines(), start=1):
            if regex.search(line):
                matches.append(f"{file_path}:{lineno}: {line.strip()}")
                if len(matches) >= max_results:
                    return ToolResult(output=truncate("\n".join(matches)))

    return ToolResult(
        output=truncate("\n".join(matches)) if matches else "No matches."
    )

pycodeloop.core.tools.bash.BashTool

Bases: Tool

Source code in pycodeloop/core/tools/bash.py
11
12
13
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
class BashTool(Tool):
    name = "bash"
    description = "Run a shell command and return its stdout/stderr."
    parameters = {
        "type": "object",
        "properties": {
            "command": {"type": "string"},
            "timeout": {"type": "integer", "default": 120},
        },
        "required": ["command"],
    }
    dangerous = True

    def preview(self, command: str, **_) -> str:
        return f"$ {command}"

    def run(self, command: str, timeout: int = 120) -> ToolResult:
        try:
            proc = subprocess.run(
                command,
                shell=True,
                capture_output=True,
                text=True,
                timeout=timeout,
            )
        except subprocess.TimeoutExpired:
            return ToolResult(
                output=f"Command timed out after {timeout}s", is_error=True
            )

        output = truncate(proc.stdout + proc.stderr)

        return ToolResult(output=output, is_error=proc.returncode != 0)

dangerous = True class-attribute instance-attribute

description = 'Run a shell command and return its stdout/stderr.' class-attribute instance-attribute

name = 'bash' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {'command': {'type': 'string'}, 'timeout': {'type': 'integer', 'default': 120}}, 'required': ['command']} class-attribute instance-attribute

preview(command, **_)

Source code in pycodeloop/core/tools/bash.py
24
25
def preview(self, command: str, **_) -> str:
    return f"$ {command}"

run(command, timeout=120)

Source code in pycodeloop/core/tools/bash.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def run(self, command: str, timeout: int = 120) -> ToolResult:
    try:
        proc = subprocess.run(
            command,
            shell=True,
            capture_output=True,
            text=True,
            timeout=timeout,
        )
    except subprocess.TimeoutExpired:
        return ToolResult(
            output=f"Command timed out after {timeout}s", is_error=True
        )

    output = truncate(proc.stdout + proc.stderr)

    return ToolResult(output=output, is_error=proc.returncode != 0)

pycodeloop.core.tools.web.WebFetchTool

Bases: Tool

Source code in pycodeloop/core/tools/web.py
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
class WebFetchTool(Tool):
    name = "web_fetch"
    description = "Fetch a URL and return its content as plain text."
    parameters = {
        "type": "object",
        "properties": {
            "url": {"type": "string"},
            "timeout": {"type": "number", "default": 30},
        },
        "required": ["url"],
    }

    def run(self, url: str, timeout: float = 30) -> ToolResult:
        hostname = urlparse(url).hostname

        if not hostname or is_blocked_host(hostname):
            return ToolResult(
                output=f"Refused to fetch {url}: host is not a public address",
                is_error=True,
            )

        try:
            response = httpx.get(url, timeout=timeout, follow_redirects=False)
        except httpx.HTTPError as exc:
            return ToolResult(output=f"Error fetching {url}: {exc}", is_error=True)

        if response.is_redirect:
            return ToolResult(
                output=f"{url} redirects to "
                f"{response.headers.get('location')} — fetch that URL "
                "directly if it's safe to follow",
                is_error=True,
            )

        try:
            response.raise_for_status()
        except httpx.HTTPError as exc:
            return ToolResult(output=f"Error fetching {url}: {exc}", is_error=True)

        content_type = response.headers.get("content-type", "")
        text = _html_to_text(response.text) if "html" in content_type else response.text

        if len(text) > _MAX_CHARS:
            text = text[:_MAX_CHARS] + "\n… (truncated)"

        return ToolResult(output=text)

description = 'Fetch a URL and return its content as plain text.' class-attribute instance-attribute

name = 'web_fetch' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {'url': {'type': 'string'}, 'timeout': {'type': 'number', 'default': 30}}, 'required': ['url']} class-attribute instance-attribute

run(url, timeout=30)

Source code in pycodeloop/core/tools/web.py
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
def run(self, url: str, timeout: float = 30) -> ToolResult:
    hostname = urlparse(url).hostname

    if not hostname or is_blocked_host(hostname):
        return ToolResult(
            output=f"Refused to fetch {url}: host is not a public address",
            is_error=True,
        )

    try:
        response = httpx.get(url, timeout=timeout, follow_redirects=False)
    except httpx.HTTPError as exc:
        return ToolResult(output=f"Error fetching {url}: {exc}", is_error=True)

    if response.is_redirect:
        return ToolResult(
            output=f"{url} redirects to "
            f"{response.headers.get('location')} — fetch that URL "
            "directly if it's safe to follow",
            is_error=True,
        )

    try:
        response.raise_for_status()
    except httpx.HTTPError as exc:
        return ToolResult(output=f"Error fetching {url}: {exc}", is_error=True)

    content_type = response.headers.get("content-type", "")
    text = _html_to_text(response.text) if "html" in content_type else response.text

    if len(text) > _MAX_CHARS:
        text = text[:_MAX_CHARS] + "\n… (truncated)"

    return ToolResult(output=text)

pycodeloop.core.tools.http_request.HttpRequestTool

Bases: Tool

Source code in pycodeloop/core/tools/http_request.py
 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
class HttpRequestTool(Tool):
    name = "http_request"
    description = (
        "Call a JSON HTTP API — any method, headers, and body. Use "
        "web_fetch instead for reading a webpage's text."
    )
    parameters = {
        "type": "object",
        "properties": {
            "url": {"type": "string"},
            "method": {"type": "string", "default": "GET"},
            "headers": {"type": "object"},
            "json_body": {
                "type": "object",
                "description": "Sent as the JSON request body.",
            },
            "timeout": {"type": "number", "default": 30},
        },
        "required": ["url"],
    }
    dangerous = True

    def preview(
        self,
        url: str,
        method: str = "GET",
        headers: dict | None = None,
        json_body: dict | None = None,
        **_,
    ) -> str:
        lines = [f"$ {method.upper()} {url}"]

        if headers:
            lines.append(f"headers: {headers}")

        if json_body is not None:
            lines.append(f"body: {json_body}")

        return "\n".join(lines)

    def run(
        self,
        url: str,
        method: str = "GET",
        headers: dict | None = None,
        json_body: dict | None = None,
        timeout: float = 30,
    ) -> ToolResult:
        method = method.upper()

        if method not in _METHODS:
            return ToolResult(output=f"Unsupported method: {method}", is_error=True)

        hostname = urlparse(url).hostname
        if not hostname or is_blocked_host(hostname):
            return ToolResult(
                output=f"Refused to call {url}: host is not a public address",
                is_error=True,
            )

        try:
            response = httpx.request(
                method,
                url,
                headers=headers,
                json=json_body,
                timeout=timeout,
                follow_redirects=False,
            )
        except httpx.HTTPError as exc:
            return ToolResult(output=f"Error calling {url}: {exc}", is_error=True)

        if response.is_redirect:
            return ToolResult(
                output=f"{url} redirects to "
                f"{response.headers.get('location')} — call that URL "
                "directly if it's safe to follow",
                is_error=True,
            )

        text = response.text

        if len(text) > _MAX_CHARS:
            text = text[:_MAX_CHARS] + "\n… (truncated)"

        summary = f"{response.status_code} {response.reason_phrase}\n{text}"

        return ToolResult(output=summary, is_error=response.is_error)

dangerous = True class-attribute instance-attribute

description = "Call a JSON HTTP API — any method, headers, and body. Use web_fetch instead for reading a webpage's text." class-attribute instance-attribute

name = 'http_request' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {'url': {'type': 'string'}, 'method': {'type': 'string', 'default': 'GET'}, 'headers': {'type': 'object'}, 'json_body': {'type': 'object', 'description': 'Sent as the JSON request body.'}, 'timeout': {'type': 'number', 'default': 30}}, 'required': ['url']} class-attribute instance-attribute

preview(url, method='GET', headers=None, json_body=None, **_)

Source code in pycodeloop/core/tools/http_request.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def preview(
    self,
    url: str,
    method: str = "GET",
    headers: dict | None = None,
    json_body: dict | None = None,
    **_,
) -> str:
    lines = [f"$ {method.upper()} {url}"]

    if headers:
        lines.append(f"headers: {headers}")

    if json_body is not None:
        lines.append(f"body: {json_body}")

    return "\n".join(lines)

run(url, method='GET', headers=None, json_body=None, timeout=30)

Source code in pycodeloop/core/tools/http_request.py
 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
def run(
    self,
    url: str,
    method: str = "GET",
    headers: dict | None = None,
    json_body: dict | None = None,
    timeout: float = 30,
) -> ToolResult:
    method = method.upper()

    if method not in _METHODS:
        return ToolResult(output=f"Unsupported method: {method}", is_error=True)

    hostname = urlparse(url).hostname
    if not hostname or is_blocked_host(hostname):
        return ToolResult(
            output=f"Refused to call {url}: host is not a public address",
            is_error=True,
        )

    try:
        response = httpx.request(
            method,
            url,
            headers=headers,
            json=json_body,
            timeout=timeout,
            follow_redirects=False,
        )
    except httpx.HTTPError as exc:
        return ToolResult(output=f"Error calling {url}: {exc}", is_error=True)

    if response.is_redirect:
        return ToolResult(
            output=f"{url} redirects to "
            f"{response.headers.get('location')} — call that URL "
            "directly if it's safe to follow",
            is_error=True,
        )

    text = response.text

    if len(text) > _MAX_CHARS:
        text = text[:_MAX_CHARS] + "\n… (truncated)"

    summary = f"{response.status_code} {response.reason_phrase}\n{text}"

    return ToolResult(output=summary, is_error=response.is_error)

pycodeloop.core.tools.git.GitStatusTool

Bases: Tool

Source code in pycodeloop/core/tools/git.py
31
32
33
34
35
36
37
class GitStatusTool(Tool):
    name = "git_status"
    description = "Show the working tree status (git status --porcelain)."
    parameters = {"type": "object", "properties": {}}

    def run(self) -> ToolResult:
        return _run_git("status", "--porcelain=v1", "--branch")

description = 'Show the working tree status (git status --porcelain).' class-attribute instance-attribute

name = 'git_status' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {}} class-attribute instance-attribute

run()

Source code in pycodeloop/core/tools/git.py
36
37
def run(self) -> ToolResult:
    return _run_git("status", "--porcelain=v1", "--branch")

pycodeloop.core.tools.git.GitDiffTool

Bases: Tool

Source code in pycodeloop/core/tools/git.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
class GitDiffTool(Tool):
    name = "git_diff"
    description = "Show unstaged (or staged) changes as a unified diff."
    parameters = {
        "type": "object",
        "properties": {
            "path": {"type": "string"},
            "staged": {"type": "boolean", "default": False},
        },
    }

    def run(self, path: str = "", staged: bool = False) -> ToolResult:
        args = ["diff"]

        if staged:
            args.append("--staged")

        if path:
            args.extend(["--", path])

        return _run_git(*args)

description = 'Show unstaged (or staged) changes as a unified diff.' class-attribute instance-attribute

name = 'git_diff' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {'path': {'type': 'string'}, 'staged': {'type': 'boolean', 'default': False}}} class-attribute instance-attribute

run(path='', staged=False)

Source code in pycodeloop/core/tools/git.py
51
52
53
54
55
56
57
58
59
60
def run(self, path: str = "", staged: bool = False) -> ToolResult:
    args = ["diff"]

    if staged:
        args.append("--staged")

    if path:
        args.extend(["--", path])

    return _run_git(*args)

pycodeloop.core.tools.git.GitLogTool

Bases: Tool

Source code in pycodeloop/core/tools/git.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
class GitLogTool(Tool):
    name = "git_log"
    description = "Show recent commit history, one line per commit."
    parameters = {
        "type": "object",
        "properties": {
            "max_count": {"type": "integer", "default": 20},
            "path": {"type": "string"},
        },
    }

    def run(self, max_count: int = 20, path: str = "") -> ToolResult:
        args = ["log", f"-n{max_count}", "--oneline"]

        if path:
            args.extend(["--", path])

        return _run_git(*args)

description = 'Show recent commit history, one line per commit.' class-attribute instance-attribute

name = 'git_log' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {'max_count': {'type': 'integer', 'default': 20}, 'path': {'type': 'string'}}} class-attribute instance-attribute

run(max_count=20, path='')

Source code in pycodeloop/core/tools/git.py
74
75
76
77
78
79
80
def run(self, max_count: int = 20, path: str = "") -> ToolResult:
    args = ["log", f"-n{max_count}", "--oneline"]

    if path:
        args.extend(["--", path])

    return _run_git(*args)

pycodeloop.core.tools.git.GitCommitTool

Bases: Tool

Source code in pycodeloop/core/tools/git.py
 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
class GitCommitTool(Tool):
    name = "git_commit"
    description = (
        "Stage files and create a commit. Pass `paths` to stage specific "
        "files, or omit it to stage everything (git add -A)."
    )
    parameters = {
        "type": "object",
        "properties": {
            "message": {"type": "string"},
            "paths": {"type": "array", "items": {"type": "string"}},
        },
        "required": ["message"],
    }
    dangerous = True

    def preview(self, message: str, paths: list[str] | None = None, **_) -> str:
        stat = _run_git("diff", "--stat", "HEAD")
        target = ", ".join(paths) if paths else "all changes"
        return f"$ git commit -m {message!r} ({target})\n\n{stat.output}"

    def run(self, message: str, paths: list[str] | None = None) -> ToolResult:
        add_result = _run_git("add", *(paths or ["-A"]))

        if add_result.is_error:
            return add_result

        return _run_git("commit", "-m", message)

dangerous = True class-attribute instance-attribute

description = 'Stage files and create a commit. Pass `paths` to stage specific files, or omit it to stage everything (git add -A).' class-attribute instance-attribute

name = 'git_commit' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {'message': {'type': 'string'}, 'paths': {'type': 'array', 'items': {'type': 'string'}}}, 'required': ['message']} class-attribute instance-attribute

preview(message, paths=None, **_)

Source code in pycodeloop/core/tools/git.py
 99
100
101
102
def preview(self, message: str, paths: list[str] | None = None, **_) -> str:
    stat = _run_git("diff", "--stat", "HEAD")
    target = ", ".join(paths) if paths else "all changes"
    return f"$ git commit -m {message!r} ({target})\n\n{stat.output}"

run(message, paths=None)

Source code in pycodeloop/core/tools/git.py
104
105
106
107
108
109
110
def run(self, message: str, paths: list[str] | None = None) -> ToolResult:
    add_result = _run_git("add", *(paths or ["-A"]))

    if add_result.is_error:
        return add_result

    return _run_git("commit", "-m", message)

pycodeloop.core.tools.env.EnvTool

Bases: Tool

Source code in pycodeloop/core/tools/env.py
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
class EnvTool(Tool):
    name = "env"
    description = (
        "Read environment variables. Pass `name` for one variable, or "
        "omit it to list every variable name (values containing SECRET, "
        "KEY, TOKEN, PASSWORD, or CREDENTIAL are masked)."
    )
    parameters = {
        "type": "object",
        "properties": {
            "name": {"type": "string"},
        },
    }

    def run(self, name: str = "") -> ToolResult:
        if name:
            value = os.environ.get(name)

            if value is None:
                return ToolResult(output=f"{name} is not set", is_error=True)

            return ToolResult(output=f"{name}={_mask(name, value)}")

        lines = [
            f"{key}={_mask(key, value)}" for key, value in sorted(os.environ.items())
        ]

        return ToolResult(output="\n".join(lines))

description = 'Read environment variables. Pass `name` for one variable, or omit it to list every variable name (values containing SECRET, KEY, TOKEN, PASSWORD, or CREDENTIAL are masked).' class-attribute instance-attribute

name = 'env' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {'name': {'type': 'string'}}} class-attribute instance-attribute

run(name='')

Source code in pycodeloop/core/tools/env.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
def run(self, name: str = "") -> ToolResult:
    if name:
        value = os.environ.get(name)

        if value is None:
            return ToolResult(output=f"{name} is not set", is_error=True)

        return ToolResult(output=f"{name}={_mask(name, value)}")

    lines = [
        f"{key}={_mask(key, value)}" for key, value in sorted(os.environ.items())
    ]

    return ToolResult(output="\n".join(lines))

pycodeloop.core.tools.todo.TodoTool

Bases: Tool

Scratchpad checklist for multi-step tasks. State lives on the tool instance, so it persists for as long as this instance is reused across turns (the default — Agent builds each tool once per session).

Source code in pycodeloop/core/tools/todo.py
10
11
12
13
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
class TodoTool(Tool):
    """Scratchpad checklist for multi-step tasks. State lives on the tool
    instance, so it persists for as long as this instance is reused across
    turns (the default — `Agent` builds each tool once per session)."""

    name = "todo"
    description = (
        "Track a checklist of steps for a multi-step task. Actions: "
        "'add' (needs `text`), 'complete' (needs `item_id`), 'list', "
        "'clear'."
    )
    parameters = {
        "type": "object",
        "properties": {
            "action": {"type": "string", "enum": sorted(_ACTIONS)},
            "text": {"type": "string"},
            "item_id": {"type": "integer"},
        },
        "required": ["action"],
    }

    def __init__(self) -> None:
        self._items: list[dict] = []
        self._next_id = 1

    def _render(self) -> str:
        if not self._items:
            return "(empty)"
        return "\n".join(
            f"[{'x' if item['done'] else ' '}] {item['id']}. {item['text']}"
            for item in self._items
        )

    def run(
        self,
        action: str,
        text: str = "",
        item_id: int | None = None,
    ) -> ToolResult:
        if action not in _ACTIONS:
            return ToolResult(output=f"Unknown action: {action}", is_error=True)

        if action == "add":
            if not text:
                return ToolResult(output="text is required", is_error=True)

            self._items.append({"id": self._next_id, "text": text, "done": False})
            self._next_id += 1

            return ToolResult(output=self._render())

        if action == "complete":
            item = next((i for i in self._items if i["id"] == item_id), None)

            if item is None:
                return ToolResult(output=f"No item with id {item_id}", is_error=True)

            item["done"] = True

            return ToolResult(output=self._render())

        if action == "clear":
            self._items.clear()
            self._next_id = 1
            return ToolResult(output="(empty)")

        return ToolResult(output=self._render())

description = "Track a checklist of steps for a multi-step task. Actions: 'add' (needs `text`), 'complete' (needs `item_id`), 'list', 'clear'." class-attribute instance-attribute

name = 'todo' class-attribute instance-attribute

parameters = {'type': 'object', 'properties': {'action': {'type': 'string', 'enum': sorted(_ACTIONS)}, 'text': {'type': 'string'}, 'item_id': {'type': 'integer'}}, 'required': ['action']} class-attribute instance-attribute

__init__()

Source code in pycodeloop/core/tools/todo.py
31
32
33
def __init__(self) -> None:
    self._items: list[dict] = []
    self._next_id = 1

run(action, text='', item_id=None)

Source code in pycodeloop/core/tools/todo.py
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
def run(
    self,
    action: str,
    text: str = "",
    item_id: int | None = None,
) -> ToolResult:
    if action not in _ACTIONS:
        return ToolResult(output=f"Unknown action: {action}", is_error=True)

    if action == "add":
        if not text:
            return ToolResult(output="text is required", is_error=True)

        self._items.append({"id": self._next_id, "text": text, "done": False})
        self._next_id += 1

        return ToolResult(output=self._render())

    if action == "complete":
        item = next((i for i in self._items if i["id"] == item_id), None)

        if item is None:
            return ToolResult(output=f"No item with id {item_id}", is_error=True)

        item["done"] = True

        return ToolResult(output=self._render())

    if action == "clear":
        self._items.clear()
        self._next_id = 1
        return ToolResult(output="(empty)")

    return ToolResult(output=self._render())