core/tools: rename grep_files to regex_files (regex pattern)

This commit is contained in:
2026-09-22 23:22:13 +08:00
parent 76933b044c
commit de03e5932b
11 changed files with 35 additions and 35 deletions
+4 -4
View File
@@ -332,14 +332,14 @@ def _glob_detail(_args_json: str, result: str) -> str:
return _pretty_segments((f"({count} {unit})", None)) return _pretty_segments((f"({count} {unit})", None))
def _grep_prefix(args_json: str) -> str: def _regex_prefix(args_json: str) -> str:
pattern = _json_str_arg(args_json, "pattern") or "?" pattern = _json_str_arg(args_json, "pattern") or "?"
path = _json_str_arg(args_json, "path") or "." path = _json_str_arg(args_json, "path") or "."
return _pretty_prefix(f"* Grep '{pattern}' in '{path}' ") return _pretty_prefix(f"* Grep '{pattern}' in '{path}' ")
def _grep_detail(_args_json: str, result: str) -> str: def _regex_detail(_args_json: str, result: str) -> str:
"""``grep_files`` outcome: matches and file count, or none/error.""" """``regex_files`` outcome: matches and file count, or none/error."""
if result.startswith("error:"): if result.startswith("error:"):
return _pretty_segments((f"({result})", "red")) return _pretty_segments((f"({result})", "red"))
if result == "(no matches)": if result == "(no matches)":
@@ -802,7 +802,7 @@ _PRETTY_LINES: dict[str, PrettyLine] = {
"todo_clear": _todo_line("todo_clear"), "todo_clear": _todo_line("todo_clear"),
"listdir": PrettyLine(prefix=_listdir_prefix, detail=_listdir_detail), "listdir": PrettyLine(prefix=_listdir_prefix, detail=_listdir_detail),
"glob_paths": PrettyLine(prefix=_glob_prefix, detail=_glob_detail), "glob_paths": PrettyLine(prefix=_glob_prefix, detail=_glob_detail),
"grep_files": PrettyLine(prefix=_grep_prefix, detail=_grep_detail), "regex_files": PrettyLine(prefix=_regex_prefix, detail=_regex_detail),
"get_truncated": PrettyLine(prefix=_resume_prefix, detail=_resume_detail), "get_truncated": PrettyLine(prefix=_resume_prefix, detail=_resume_detail),
"run_argv": PrettyLine(prefix=_run_argv_prefix, detail=_run_argv_detail), "run_argv": PrettyLine(prefix=_run_argv_prefix, detail=_run_argv_detail),
"run_argv_batch": PrettyLine(prefix=_run_argv_batch_prefix, detail=_run_argv_batch_detail), "run_argv_batch": PrettyLine(prefix=_run_argv_batch_prefix, detail=_run_argv_batch_detail),
+1 -1
View File
@@ -37,10 +37,10 @@ from .file import delete_path as delete_path
from .file import edit_lineno as edit_lineno from .file import edit_lineno as edit_lineno
from .file import edit_replace as edit_replace from .file import edit_replace as edit_replace
from .file import glob_paths as glob_paths from .file import glob_paths as glob_paths
from .file import grep_files as grep_files
from .file import listdir as listdir from .file import listdir as listdir
from .file import move_path as move_path from .file import move_path as move_path
from .file import read_file as read_file from .file import read_file as read_file
from .file import regex_files as regex_files
from .file import tree as tree from .file import tree as tree
from .file import write_file as write_file from .file import write_file as write_file
from .net import NET_TOOLS as NET_TOOLS from .net import NET_TOOLS as NET_TOOLS
+2 -2
View File
@@ -7,9 +7,9 @@ from .fs_ops import delete_path as delete_path
from .fs_ops import move_path as move_path from .fs_ops import move_path as move_path
from .get_truncated import get_truncated as get_truncated from .get_truncated import get_truncated as get_truncated
from .glob_paths import glob_paths as glob_paths from .glob_paths import glob_paths as glob_paths
from .grep_files import grep_files as grep_files
from .listdir import listdir as listdir from .listdir import listdir as listdir
from .read import read_file as read_file from .read import read_file as read_file
from .regex_files import regex_files as regex_files
from .tree import tree as tree from .tree import tree as tree
from .write import write_file as write_file from .write import write_file as write_file
@@ -19,7 +19,7 @@ FILE_TOOLS = [
listdir, listdir,
tree, tree,
glob_paths, glob_paths,
grep_files, regex_files,
edit_replace, edit_replace,
edit_lineno, edit_lineno,
copy_path, copy_path,
@@ -130,7 +130,7 @@ def _compile_pattern(pattern: str, *, case_insensitive: bool) -> re.Pattern[str]
@tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.READ_ONLY) @tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.READ_ONLY)
async def grep_files( async def regex_files(
pattern: str, pattern: str,
path: str = ".", path: str = ".",
*, *,
+8 -8
View File
@@ -5,7 +5,7 @@ optional bundled files — scripts, references, templates. Discovery, parsing, a
path rules live in :mod:`plyngent.skills`; this module is the model-facing shell. path rules live in :mod:`plyngent.skills`; this module is the model-facing shell.
Reading needs no grant: the host registers the roots as static read-only roots, Reading needs no grant: the host registers the roots as static read-only roots,
so ``read_file`` / ``grep_files`` see them too. Writing goes through the skill so ``read_file`` / ``regex_files`` see them too. Writing goes through the skill
tools' own grant (a per-call confirm, or ``[skills].allow_write``) and is always tools' own grant (a per-call confirm, or ``[skills].allow_write``) and is always
confined to a skill root — that is why these tools never inherit the confined to a skill root — that is why these tools never inherit the
directory-access path of ``request_directory_access``. directory-access path of ``request_directory_access``.
@@ -30,9 +30,9 @@ from plyngent.skills import (
resolve_skill_file, resolve_skill_file,
validate_skill_name, validate_skill_name,
) )
from plyngent.tools.file.grep_files import DEFAULT_MAX_MATCHES
from plyngent.tools.file.grep_files import grep_files as _grep_files
from plyngent.tools.file.read import read_file as _read_file from plyngent.tools.file.read import read_file as _read_file
from plyngent.tools.file.regex_files import DEFAULT_MAX_MATCHES
from plyngent.tools.file.regex_files import regex_files as _regex_files
from plyngent.tools.workspace import ( from plyngent.tools.workspace import (
DEFAULT_POLICY_CONFIRM_TIMEOUT_SECONDS, DEFAULT_POLICY_CONFIRM_TIMEOUT_SECONDS,
WorkspaceError, WorkspaceError,
@@ -47,7 +47,7 @@ if TYPE_CHECKING:
# Keeps a listing readable when a third-party root holds a pile of skills. # Keeps a listing readable when a third-party root holds a pile of skills.
_MAX_LISTED_ISSUES = 8 _MAX_LISTED_ISSUES = 8
_SKILL_WRITE_POLICY_KEY = "skill_write_policy" _SKILL_WRITE_POLICY_KEY = "skill_write_policy"
# ``path:line: content`` as ``grep_files`` formats it (the path is non-greedy so # ``path:line: content`` as ``regex_files`` formats it (the path is non-greedy so
# a Windows drive letter or a colon in a file name cannot split it wrongly). # a Windows drive letter or a colon in a file name cannot split it wrongly).
_HIT_RE = re.compile(r"^(?P<path>.+?):(?P<line>\d+): ?(?P<content>.*)$") _HIT_RE = re.compile(r"^(?P<path>.+?):(?P<line>\d+): ?(?P<content>.*)$")
@@ -159,9 +159,9 @@ async def skill_read(
def _hit_relative(skill: Skill, path: str) -> str: def _hit_relative(skill: Skill, path: str) -> str:
"""Rebase one ``grep_files`` hit path onto its skill directory. """Rebase one ``regex_files`` hit path onto its skill directory.
Grep reports paths relative to the workspace root for files inside it and The search reports paths relative to the workspace root for files inside it and
absolute paths for files outside, so a skill directory can arrive either way absolute paths for files outside, so a skill directory can arrive either way
(a project-scope skill root lives inside the workspace). Normalizing to an (a project-scope skill root lives inside the workspace). Normalizing to an
absolute path first covers both without guessing from the string alone. absolute path first covers both without guessing from the string alone.
@@ -179,7 +179,7 @@ def _hit_relative(skill: Skill, path: str) -> str:
def _skill_hits(skill: Skill, result: str) -> list[str]: def _skill_hits(skill: Skill, result: str) -> list[str]:
"""Attribute one skill's grep lines to that skill.""" """Attribute one skill's search lines to that skill."""
hits: list[str] = [] hits: list[str] = []
for line in result.splitlines(): for line in result.splitlines():
match = _HIT_RE.match(line) match = _HIT_RE.match(line)
@@ -229,7 +229,7 @@ async def skill_search(
remaining = max_matches - len(hits) remaining = max_matches - len(hits)
if remaining <= 0: if remaining <= 0:
break break
result = await _grep_files.handler( result = await _regex_files.handler(
pattern, pattern,
str(target.path), str(target.path),
case_insensitive=case_insensitive, case_insensitive=case_insensitive,
+1 -1
View File
@@ -79,7 +79,7 @@ def test_parallel_tool_outputs_interleaved_after_calls() -> None:
), ),
AssistantFunctionToolCall( AssistantFunctionToolCall(
id="call_02_ABCD", id="call_02_ABCD",
function=AssistantFunctionTool(name="grep_files", arguments='{"pattern":"todo"}'), function=AssistantFunctionTool(name="regex_files", arguments='{"pattern":"todo"}'),
), ),
], ],
), ),
+2 -2
View File
@@ -352,13 +352,13 @@ async def test_pretty_glob_truncated(capsys: pytest.CaptureFixture[str]) -> None
async def test_pretty_grep(capsys: pytest.CaptureFixture[str]) -> None: async def test_pretty_grep(capsys: pytest.CaptureFixture[str]) -> None:
result = "src/a.py:3: x = 1\nsrc/a.py:9: x = 2\nsrc/b.py:1: y = 3\n" result = "src/a.py:3: x = 1\nsrc/a.py:9: x = 2\nsrc/b.py:1: y = 3\n"
await render_events(_aiter([_pretty_call("grep_files", '{"pattern": "x"}'), _result(result)])) await render_events(_aiter([_pretty_call("regex_files", '{"pattern": "x"}'), _result(result)]))
out = capsys.readouterr().out out = capsys.readouterr().out
assert "* Grep 'x' in '.' (3 matches in 2 files)" in out assert "* Grep 'x' in '.' (3 matches in 2 files)" in out
async def test_pretty_grep_no_matches(capsys: pytest.CaptureFixture[str]) -> None: async def test_pretty_grep_no_matches(capsys: pytest.CaptureFixture[str]) -> None:
await render_events(_aiter([_pretty_call("grep_files", '{"pattern": "zzz"}'), _result("(no matches)")])) await render_events(_aiter([_pretty_call("regex_files", '{"pattern": "zzz"}'), _result("(no matches)")]))
out = capsys.readouterr().out out = capsys.readouterr().out
assert "* Grep 'zzz' in '.' (no matches)" in out assert "* Grep 'zzz' in '.' (no matches)" in out
+2 -2
View File
@@ -61,7 +61,7 @@ def test_yaml_subset_types_lists_and_booleans() -> None:
"read_only: true", "read_only: true",
"confirm: no", "confirm: no",
"retries: 3", "retries: 3",
"allowed-tools: [read_file, grep_files]", "allowed-tools: [read_file, regex_files]",
"tags:", "tags:",
" - alpha", " - alpha",
" - beta", " - beta",
@@ -72,7 +72,7 @@ def test_yaml_subset_types_lists_and_booleans() -> None:
"read_only": True, "read_only": True,
"confirm": False, "confirm": False,
"retries": 3, "retries": 3,
"allowed-tools": ["read_file", "grep_files"], "allowed-tools": ["read_file", "regex_files"],
"tags": ["alpha", "beta"], "tags": ["alpha", "beta"],
} }
+1 -1
View File
@@ -61,7 +61,7 @@ def test_read_only_classification() -> None:
"read_file", "read_file",
"get_truncated", "get_truncated",
"glob_paths", "glob_paths",
"grep_files", "regex_files",
"listdir", "listdir",
"tree", "tree",
"todo_list", "todo_list",
@@ -2,46 +2,46 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from plyngent.tools.file.grep_files import grep_files from plyngent.tools.file.regex_files import regex_files
from tests.test_tools.helpers import call_sync from tests.test_tools.helpers import call_sync
def test_grep_files_basic(workspace: object) -> None: def test_regex_files_basic(workspace: object) -> None:
assert isinstance(workspace, Path) assert isinstance(workspace, Path)
_ = (workspace / "a.py").write_text("hello world\nfoo\n", encoding="utf-8") _ = (workspace / "a.py").write_text("hello world\nfoo\n", encoding="utf-8")
_ = (workspace / "b.py").write_text("nope\n", encoding="utf-8") _ = (workspace / "b.py").write_text("nope\n", encoding="utf-8")
out = call_sync(grep_files, "hello") out = call_sync(regex_files, "hello")
assert "a.py:1:" in out assert "a.py:1:" in out
assert "hello world" in out assert "hello world" in out
assert "b.py" not in out assert "b.py" not in out
def test_grep_files_case_insensitive(workspace: object) -> None: def test_regex_files_case_insensitive(workspace: object) -> None:
assert isinstance(workspace, Path) assert isinstance(workspace, Path)
_ = (workspace / "t.txt").write_text("Hello\n", encoding="utf-8") _ = (workspace / "t.txt").write_text("Hello\n", encoding="utf-8")
assert "no matches" in call_sync(grep_files, "hello") assert "no matches" in call_sync(regex_files, "hello")
out = call_sync(grep_files, "hello", case_insensitive=True) out = call_sync(regex_files, "hello", case_insensitive=True)
assert "t.txt:1:" in out assert "t.txt:1:" in out
def test_grep_files_invalid_regex(workspace: object) -> None: def test_regex_files_invalid_regex(workspace: object) -> None:
del workspace del workspace
assert "invalid regex" in call_sync(grep_files, "[") assert "invalid regex" in call_sync(regex_files, "[")
def test_grep_files_skip_git(workspace: object) -> None: def test_regex_files_skip_git(workspace: object) -> None:
assert isinstance(workspace, Path) assert isinstance(workspace, Path)
(workspace / ".git").mkdir() (workspace / ".git").mkdir()
_ = (workspace / ".git" / "x").write_text("secret\n", encoding="utf-8") _ = (workspace / ".git" / "x").write_text("secret\n", encoding="utf-8")
_ = (workspace / "ok.txt").write_text("secret\n", encoding="utf-8") _ = (workspace / "ok.txt").write_text("secret\n", encoding="utf-8")
out = call_sync(grep_files, "secret") out = call_sync(regex_files, "secret")
assert "ok.txt" in out assert "ok.txt" in out
assert ".git" not in out assert ".git" not in out
def test_grep_files_max_matches(workspace: object) -> None: def test_regex_files_max_matches(workspace: object) -> None:
assert isinstance(workspace, Path) assert isinstance(workspace, Path)
lines = "\n".join(f"match {i}" for i in range(10)) lines = "\n".join(f"match {i}" for i in range(10))
_ = (workspace / "m.txt").write_text(lines + "\n", encoding="utf-8") _ = (workspace / "m.txt").write_text(lines + "\n", encoding="utf-8")
out = call_sync(grep_files, "match", max_matches=3) out = call_sync(regex_files, "match", max_matches=3)
assert "truncated" in out assert "truncated" in out
+1 -1
View File
@@ -198,7 +198,7 @@ async def test_skill_search_single_skill_and_errors(tmp_path: Path) -> None:
async def test_skill_search_inside_the_workspace_rebases_the_hit(tmp_path: Path) -> None: async def test_skill_search_inside_the_workspace_rebases_the_hit(tmp_path: Path) -> None:
"""A project-scope skill root lives inside the workspace: one path, once. """A project-scope skill root lives inside the workspace: one path, once.
Grep reports in-workspace files relative to the workspace root and outside The search reports in-workspace files relative to the workspace root and outside
ones absolutely, so the skill name must never be glued onto a path that ones absolutely, so the skill name must never be glued onto a path that
already carries the root. already carries the root.
""" """