Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
| """ | |
| Local tool implementations β bash/read/write/edit running on the user's machine. | |
| Drop-in replacement for sandbox tools when running in CLI (local) mode. | |
| Same tool specs (names, parameters) but handlers execute locally via | |
| subprocess/pathlib instead of going through a remote sandbox. | |
| """ | |
| from __future__ import annotations | |
| import subprocess | |
| from pathlib import Path | |
| from typing import Any | |
| from agent.tools.sandbox_client import Sandbox | |
| MAX_OUTPUT_CHARS = 30_000 | |
| MAX_LINE_LENGTH = 2000 | |
| DEFAULT_READ_LINES = 2000 | |
| DEFAULT_TIMEOUT = 120 | |
| MAX_TIMEOUT = 600 | |
| # ββ Handlers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async def _bash_handler(args: dict[str, Any], **_kw) -> tuple[str, bool]: | |
| command = args.get("command", "") | |
| if not command: | |
| return "No command provided.", False | |
| work_dir = args.get("work_dir", ".") | |
| timeout = min(args.get("timeout") or DEFAULT_TIMEOUT, MAX_TIMEOUT) | |
| try: | |
| result = subprocess.run( | |
| command, | |
| shell=True, | |
| capture_output=True, | |
| text=True, | |
| cwd=work_dir, | |
| timeout=timeout, | |
| ) | |
| output = result.stdout + result.stderr | |
| if len(output) > MAX_OUTPUT_CHARS: | |
| output = output[:MAX_OUTPUT_CHARS] + "\n... (output truncated)" | |
| if not output.strip(): | |
| output = "(no output)" | |
| return output, result.returncode == 0 | |
| except subprocess.TimeoutExpired: | |
| return f"Command timed out after {timeout}s.", False | |
| except Exception as e: | |
| return f"bash error: {e}", False | |
| async def _read_handler(args: dict[str, Any], **_kw) -> tuple[str, bool]: | |
| file_path = args.get("path", "") | |
| if not file_path: | |
| return "No path provided.", False | |
| p = Path(file_path) | |
| if not p.exists(): | |
| return f"File not found: {file_path}", False | |
| if p.is_dir(): | |
| return "Cannot read a directory. Use bash with 'ls' instead.", False | |
| try: | |
| lines = p.read_text().splitlines() | |
| except Exception as e: | |
| return f"read error: {e}", False | |
| offset = max((args.get("offset") or 1), 1) | |
| limit = args.get("limit") or DEFAULT_READ_LINES | |
| selected = lines[offset - 1 : offset - 1 + limit] | |
| numbered = [] | |
| for i, line in enumerate(selected, start=offset): | |
| if len(line) > MAX_LINE_LENGTH: | |
| line = line[:MAX_LINE_LENGTH] + "..." | |
| numbered.append(f"{i:>6}\t{line}") | |
| return "\n".join(numbered), True | |
| async def _write_handler(args: dict[str, Any], **_kw) -> tuple[str, bool]: | |
| file_path = args.get("path", "") | |
| content = args.get("content", "") | |
| if not file_path: | |
| return "No path provided.", False | |
| p = Path(file_path) | |
| try: | |
| p.parent.mkdir(parents=True, exist_ok=True) | |
| p.write_text(content) | |
| return f"Wrote {len(content)} bytes to {file_path}", True | |
| except Exception as e: | |
| return f"write error: {e}", False | |
| async def _edit_handler(args: dict[str, Any], **_kw) -> tuple[str, bool]: | |
| file_path = args.get("path", "") | |
| old_str = args.get("old_str", "") | |
| new_str = args.get("new_str", "") | |
| replace_all = args.get("replace_all", False) | |
| if not file_path: | |
| return "No path provided.", False | |
| if old_str == new_str: | |
| return "old_str and new_str must differ.", False | |
| p = Path(file_path) | |
| if not p.exists(): | |
| return f"File not found: {file_path}", False | |
| try: | |
| text = p.read_text() | |
| except Exception as e: | |
| return f"edit read error: {e}", False | |
| count = text.count(old_str) | |
| if count == 0: | |
| return "old_str not found in file.", False | |
| if count > 1 and not replace_all: | |
| return ( | |
| f"old_str appears {count} times. Use replace_all=true to replace all, " | |
| "or provide a more specific old_str." | |
| ), False | |
| new_text = text.replace(old_str, new_str) if replace_all else text.replace(old_str, new_str, 1) | |
| try: | |
| p.write_text(new_text) | |
| except Exception as e: | |
| return f"edit write error: {e}", False | |
| replacements = count if replace_all else 1 | |
| return f"Edited {file_path} ({replacements} replacement{'s' if replacements > 1 else ''})", True | |
| # ββ Public API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _HANDLERS = { | |
| "bash": _bash_handler, | |
| "read": _read_handler, | |
| "write": _write_handler, | |
| "edit": _edit_handler, | |
| } | |
| def get_local_tools(): | |
| """Return local ToolSpecs for bash/read/write/edit (no sandbox_create).""" | |
| from agent.core.tools import ToolSpec | |
| tools = [] | |
| for name, spec in Sandbox.TOOLS.items(): | |
| handler = _HANDLERS.get(name) | |
| if handler is None: | |
| continue | |
| tools.append( | |
| ToolSpec( | |
| name=name, | |
| description=spec["description"], | |
| parameters=spec["parameters"], | |
| handler=handler, | |
| ) | |
| ) | |
| return tools | |