diff --git a/.gitignore b/.gitignore index 83972fa..4ba5426 100644 --- a/.gitignore +++ b/.gitignore @@ -216,3 +216,4 @@ __marimo__/ # Streamlit .streamlit/secrets.toml +.fastcontext/ diff --git a/src/fastcontext/agent/tool/glob.py b/src/fastcontext/agent/tool/glob.py index 6f82f79..ba4e419 100644 --- a/src/fastcontext/agent/tool/glob.py +++ b/src/fastcontext/agent/tool/glob.py @@ -2,7 +2,7 @@ import subprocess from pathlib import Path -from .tool import Tool +from .tool import Tool, resolve_path def run(directory: str, pattern: str, cwd: str) -> str: @@ -39,7 +39,7 @@ class GlobTool(Tool): async def call(self, parameters: str, **kwargs) -> str: cwd = kwargs.get("cwd", Path.cwd().as_posix()) params: dict = json.loads(parameters) - directory = params.get("directory", cwd) + directory = resolve_path(params.get("directory", cwd), cwd) pattern = params.get("pattern") p = Path(directory) diff --git a/src/fastcontext/agent/tool/grep.py b/src/fastcontext/agent/tool/grep.py index 72d5b65..c13f7a3 100644 --- a/src/fastcontext/agent/tool/grep.py +++ b/src/fastcontext/agent/tool/grep.py @@ -1,7 +1,8 @@ import json +import shutil from pathlib import Path -from .tool import Tool +from .tool import Tool, resolve_path class GrepTool(Tool): @@ -64,8 +65,8 @@ class GrepTool(Tool): "required": ["pattern"], } - # Adjust this path if ripgrep is not in your system PATH - _rg_path = "/usr/bin/rg" + # Locate ripgrep on PATH (falls back to /usr/bin/rg) + _rg_path = shutil.which("rg") or "/usr/bin/rg" async def call(self, parameters: str, **kwargs) -> str: params: dict = json.loads(parameters) @@ -84,6 +85,7 @@ async def call(self, parameters: str, **kwargs) -> str: head_limit = params.get("head_limit") multiline = params.get("multiline") + path = resolve_path(path, cwd) if not Path(path).resolve().is_relative_to(Path(cwd).resolve()): return f"Permission error: `{path}` is not within the working directory `{cwd}`." diff --git a/src/fastcontext/agent/tool/read.py b/src/fastcontext/agent/tool/read.py index b2172e2..1308ea1 100644 --- a/src/fastcontext/agent/tool/read.py +++ b/src/fastcontext/agent/tool/read.py @@ -3,7 +3,7 @@ import aiofiles -from .tool import Tool +from .tool import Tool, resolve_path MAX_LINE = 2000 MAX_LINE_LENGTH = 2000 @@ -40,6 +40,13 @@ async def call(self, parameters: str, **kwargs) -> str: if not file_path: return "Read Tool: file path is required." + cwd = kwargs.get("cwd", Path.cwd().as_posix()) + file_path = resolve_path(file_path, cwd) + + if Path(file_path).is_dir(): + entries = sorted(p.name + ("/" if p.is_dir() else "") for p in Path(file_path).iterdir()) + return f"Read Tool: {file_path} is a directory. Contents:\n" + "\n".join(entries) + if not Path(file_path).exists(): return f"Read Tool: file {file_path} does not exist." diff --git a/src/fastcontext/agent/tool/tool.py b/src/fastcontext/agent/tool/tool.py index 721b21f..18781f7 100644 --- a/src/fastcontext/agent/tool/tool.py +++ b/src/fastcontext/agent/tool/tool.py @@ -11,6 +11,34 @@ MAX_TOOLRUN_TIMEOUT = 10 +def resolve_path(path: str, cwd: str) -> str: + """Resolve model-emitted paths against the working directory. + + The model was trained on SWE-bench instances with repos mounted at + //, so it often emits paths like /myrepo/src/x.py that do + not exist locally. Remap those onto cwd when the literal path is absent. + """ + if not path: + return cwd + p = Path(path) + base = Path(cwd) + if not p.is_absolute(): + return (base / p).as_posix() + if p.exists(): + return path + parts = p.parts # ('/', 'repo', 'src', ...) + if len(parts) >= 2: + # /[/rest] -> cwd[/rest] + candidate = base.joinpath(*parts[2:]) if len(parts) > 2 else base + if parts[1] == base.name or candidate.exists(): + return candidate.as_posix() + # /src/... -> cwd/src/... + candidate = base.joinpath(*parts[1:]) + if candidate.exists(): + return candidate.as_posix() + return path + + class ToolResult(BaseModel): tool_call_id: str output: str