core/tools/file: read, write, listdir, edit_replace

Module-level @tool handlers under workspace root; first-occurrence
string replace.
This commit is contained in:
2026-07-14 18:20:32 +08:00
parent 111a554e80
commit 94720f4e57
5 changed files with 80 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
from .edit_replace import edit_replace as edit_replace
from .listdir import listdir as listdir
from .read import read_file as read_file
from .write import write_file as write_file
FILE_TOOLS = [read_file, write_file, listdir, edit_replace]
+20
View File
@@ -0,0 +1,20 @@
from __future__ import annotations
from plyngent.agent import tool
from plyngent.tools.workspace import resolve_path
@tool
def edit_replace(path: str, old_string: str, new_string: str) -> str:
"""Replace the first occurrence of ``old_string`` with ``new_string`` in a file."""
if not old_string:
return "error: old_string must not be empty"
target = resolve_path(path)
if not target.is_file():
return f"error: not a file: {path}"
text = target.read_text(encoding="utf-8", errors="replace")
if old_string not in text:
return "error: old_string not found in file"
updated = text.replace(old_string, new_string, 1)
_ = target.write_text(updated, encoding="utf-8")
return f"replaced first occurrence in {path}"
+17
View File
@@ -0,0 +1,17 @@
from __future__ import annotations
from plyngent.agent import tool
from plyngent.tools.workspace import resolve_path
@tool
def listdir(path: str = ".") -> str:
"""List entries in a directory under the workspace (name and type)."""
target = resolve_path(path)
if not target.is_dir():
return f"error: not a directory: {path}"
lines: list[str] = []
for entry in sorted(target.iterdir(), key=lambda p: p.name):
kind = "dir" if entry.is_dir() else "file"
lines.append(f"{kind}\t{entry.name}")
return "\n".join(lines) if lines else "(empty)"
+24
View File
@@ -0,0 +1,24 @@
from __future__ import annotations
from plyngent.agent import tool
from plyngent.tools.workspace import resolve_path
@tool
def read_file(path: str, *, offset: int = 0, limit: int | None = None) -> str:
"""Read a text file under the workspace.
``offset`` is 0-based line start; ``limit`` is max lines (None = rest of file).
"""
target = resolve_path(path)
if not target.is_file():
return f"error: not a file: {path}"
text = target.read_text(encoding="utf-8", errors="replace")
lines = text.splitlines(keepends=True)
if offset < 0:
return "error: offset must be >= 0"
start = offset
end = len(lines) if limit is None else min(len(lines), start + limit)
if start >= len(lines):
return ""
return "".join(lines[start:end])
+13
View File
@@ -0,0 +1,13 @@
from __future__ import annotations
from plyngent.agent import tool
from plyngent.tools.workspace import resolve_path
@tool
def write_file(path: str, content: str) -> str:
"""Write text content to a file under the workspace (creates parents)."""
target = resolve_path(path)
target.parent.mkdir(parents=True, exist_ok=True)
_ = target.write_text(content, encoding="utf-8")
return f"wrote {len(content)} characters to {path}"