"""File/exec tool implementations for agentic build benches. These are the tools the agent can call during a multi-turn build loop: list_files, read_file, write_file, edit_file, search, run_build, run_test, run_shell. They operate on a work dir set via `set_work_dir()`. The tool SCHEMA (OpenAI function-calling format) is in TOOLS; the implementations are in TOOL_IMPLS. Instruments that want a different tool set (e.g. agentic_bench_v1 uses read-only exploration + submit_fix) define their own and don't import these. """ import os import re import subprocess import shutil BUILD_TIMEOUT = int(os.environ.get("BUILD_TIMEOUT", "180")) TEST_TIMEOUT = int(os.environ.get("TEST_TIMEOUT", "180")) SHELL_TIMEOUT = int(os.environ.get("SHELL_TIMEOUT", "60")) _WORK = None def set_work_dir(d): global _WORK _WORK = d def get_work_dir(): return _WORK def materialize(files, dest=None): """Write a dict of {relative_path: content} into a work dir (cleared first).""" d = dest or _WORK shutil.rmtree(d, ignore_errors=True) os.makedirs(d, exist_ok=True) for path, content in files.items(): fp = os.path.join(d, path) os.makedirs(os.path.dirname(fp), exist_ok=True) with open(fp, "w") as f: f.write(content) return d def _run(cmd, timeout): try: p = subprocess.run(cmd, cwd=_WORK, shell=True, capture_output=True, text=True, timeout=timeout) except subprocess.TimeoutExpired: return f"TIMEOUT after {timeout}s" out = (p.stdout + "\n" + p.stderr).strip() return f"exit={p.returncode}\n{out}" def _list_files(args): out = [] for root, dirs, files in os.walk(_WORK): dirs[:] = sorted(d for d in dirs if d not in {".git", "node_modules", ".next", "target", "dist"}) for f in sorted(files): full = os.path.join(root, f) rel = os.path.relpath(full, _WORK) out.append(rel) return "\n".join(out) def _read_file(args): p = args.get("path", "") fp = os.path.join(_WORK, p) if not os.path.isfile(fp): return f"ERROR: no such file {p!r}. Use list_files." with open(fp) as f: return f.read() def _write_file(args): p = args.get("path", "") content = args.get("content", "") fp = os.path.join(_WORK, p) os.makedirs(os.path.dirname(fp), exist_ok=True) with open(fp, "w") as f: f.write(content) return f"wrote {p} ({len(content)} bytes)" def _edit_file(args): p = args.get("path", "") old = args.get("old_str", "") new = args.get("new_str", "") fp = os.path.join(_WORK, p) if not os.path.isfile(fp): return f"ERROR: no such file {p!r}." with open(fp) as f: content = f.read() count = content.count(old) if count == 0: return f"ERROR: old_str not found in {p}." if count > 1: return f"ERROR: old_str appears {count} times in {p}; make it unique." new_content = content.replace(old, new, 1) with open(fp, "w") as f: f.write(new_content) return f"edited {p} ({len(old)} -> {len(new)} chars)" def _search(args): pat = args.get("pattern", "") try: rx = re.compile(pat) except re.error as e: return f"ERROR: bad regex: {e}" hits = [] for root, dirs, files in os.walk(_WORK): dirs[:] = sorted(d for d in dirs if d not in {".git", "node_modules", ".next", "target", "dist"}) for f in sorted(files): full = os.path.join(root, f) rel = os.path.relpath(full, _WORK) with open(full, errors="replace") as fh: for i, line in enumerate(fh, 1): if rx.search(line): hits.append(f"{rel}:{i}: {line.strip()}") return "\n".join(hits) if hits else "(no matches)" def _run_build(args): return _run("go build ./...", BUILD_TIMEOUT) def _run_test(args): return _run("go test ./...", TEST_TIMEOUT) def _run_shell(args): cmd = args.get("cmd", "") if not cmd: return "ERROR: empty cmd" return _run(cmd, SHELL_TIMEOUT) TOOL_IMPLS = { "list_files": _list_files, "read_file": _read_file, "write_file": _write_file, "edit_file": _edit_file, "search": _search, "run_build": _run_build, "run_test": _run_test, "run_shell": _run_shell, } TOOLS = [ {"type": "function", "function": { "name": "list_files", "description": "List all file paths in the work dir. Returns one path per line.", "parameters": {"type": "object", "properties": {}}, }}, {"type": "function", "function": { "name": "read_file", "description": "Return the full contents of one file in the work dir.", "parameters": {"type": "object", "properties": { "path": {"type": "string", "description": "Project-relative file path"}, }, "required": ["path"]}, }}, {"type": "function", "function": { "name": "write_file", "description": "Create or overwrite a file in the work dir with the given contents.", "parameters": {"type": "object", "properties": { "path": {"type": "string"}, "content": {"type": "string", "description": "Full file contents to write"}, }, "required": ["path", "content"]}, }}, {"type": "function", "function": { "name": "edit_file", "description": "Apply a patch-style edit: replace the first occurrence of old_str with new_str. Fails if old_str is not found or appears more than once.", "parameters": {"type": "object", "properties": { "path": {"type": "string"}, "old_str": {"type": "string", "description": "The exact text to find (must be unique)"}, "new_str": {"type": "string", "description": "The replacement text"}, }, "required": ["path", "old_str", "new_str"]}, }}, {"type": "function", "function": { "name": "search", "description": "Regex-search the whole work dir; returns matching path:line: text.", "parameters": {"type": "object", "properties": { "pattern": {"type": "string"}, }, "required": ["pattern"]}, }}, {"type": "function", "function": { "name": "run_build", "description": f"Run `go build ./...` in the work dir. Returns exit code + output. Timeout {BUILD_TIMEOUT}s.", "parameters": {"type": "object", "properties": {}}, }}, {"type": "function", "function": { "name": "run_test", "description": f"Run `go test ./...` in the work dir. Returns exit code + output. Timeout {TEST_TIMEOUT}s.", "parameters": {"type": "object", "properties": {}}, }}, {"type": "function", "function": { "name": "run_shell", "description": f"Run an arbitrary shell command in the work dir (cwd = work dir). Use for `go get`, `go mod tidy`, npm, cargo, etc. Returns exit code + output. Timeout {SHELL_TIMEOUT}s.", "parameters": {"type": "object", "properties": { "cmd": {"type": "string", "description": "The shell command to run (sh -c)"}, }, "required": ["cmd"]}, }}, ]