agent harness initialize repo

This commit is contained in:
Roger Oriol
2026-07-19 20:13:54 +02:00
commit 42475d3249
53 changed files with 7391 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
from tools.registry import get_tool_registry, get_tool_schemas
__all__ = ["get_tool_registry", "get_tool_schemas"]

View File

@@ -0,0 +1,61 @@
import glob as glob_module
import re
from pathlib import Path
def read_file(path: str, offset: int = 1, limit: int = 200) -> str:
"""Read lines from a file, with optional offset and limit."""
p = Path(path)
if not p.exists():
return f"Error: file not found: {path}"
elif p.is_dir():
return f"Error: cannot read directory content using read_file for path: {path}"
else:
# Proceed with file reading logic
lines = p.read_text(errors="replace").splitlines()
selected = lines[offset - 1: offset - 1 + limit]
return "\n".join(f"{offset + i}: {line}" for i, line in enumerate(selected))
def glob_files(pattern: str, path: str = ".") -> str:
"""Find files matching a glob pattern inside a directory."""
matches = glob_module.glob(f"{path}/**/{pattern}", recursive=True)
matches += glob_module.glob(f"{path}/{pattern}")
unique = sorted(set(matches))
return "\n".join(unique) if unique else "(no matches)"
def grep(pattern: str, path: str = ".", include: str = "*") -> str:
"""Search file contents for a regex pattern, optionally filtering by filename glob."""
results = []
for filepath in glob_module.glob(f"{path}/**/{include}", recursive=True):
fp = Path(filepath)
if not fp.is_file():
continue
try:
for i, line in enumerate(fp.read_text(errors="replace").splitlines(), 1):
if re.search(pattern, line):
results.append(f"{filepath}:{i}: {line}")
except OSError:
pass
return "\n".join(results) if results else "(no matches)"
def write_file(path: str, content: str) -> str:
"""Write content to a file, creating it if it does not exist."""
p = Path(path)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(content)
return f"Wrote {len(content)} bytes to {path}"
def edit_file(path: str, old_string: str, new_string: str) -> str:
"""Replace the first occurrence of old_string with new_string in a file."""
p = Path(path)
if not p.exists():
return f"Error: file not found: {path}"
original = p.read_text()
if old_string not in original:
return f"Error: string not found in {path}"
p.write_text(original.replace(old_string, new_string, 1))
return f"Edited {path}"

View File

@@ -0,0 +1,8 @@
def ask_question(question: str) -> str:
"""Ask the user a clarifying question and return their answer."""
print(f"\n [agent] {question}")
try:
answer = input(" Your answer: ").strip()
except EOFError:
return "(no answer — EOF)"
return answer if answer else "(no answer provided)"

View File

@@ -0,0 +1,282 @@
from tools.filesystem import read_file, glob_files, grep, write_file, edit_file
from tools.shell import run_bash
from tools.web import webfetch
from tools.todo import todo_append, todo_list, todo_update
from tools.scratchpad import read_scratchpad, write_scratchpad
from tools.interaction import ask_question
def get_tool_registry():
return {
"run_bash": run_bash,
"read_file": read_file,
"glob_files": glob_files,
"grep": grep,
"write_file": write_file,
"edit_file": edit_file,
"webfetch": webfetch,
"todo_append": todo_append,
"todo_list": todo_list,
"todo_update": todo_update,
"read_scratchpad": read_scratchpad,
"write_scratchpad": write_scratchpad,
"ask_question": ask_question,
}
def get_tool_schemas():
return [
{
"type": "function",
"function": {
"name": "run_bash",
"description": "Run a bash command on the user's machine and return the output.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The bash command to execute.",
}
},
"required": ["command"],
},
},
},
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read lines from a file. Returns lines prefixed with line numbers.",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Absolute or relative path to the file."},
"offset": {"type": "integer", "description": "First line to read (1-indexed). Defaults to 1."},
"limit": {"type": "integer", "description": "Maximum number of lines to return. Defaults to 200."},
},
"required": ["path"],
},
},
},
{
"type": "function",
"function": {
"name": "glob_files",
"description": "Find files matching a glob pattern (e.g. '**/*.py') inside a directory.",
"parameters": {
"type": "object",
"properties": {
"pattern": {"type": "string", "description": "Glob pattern to match against file names."},
"path": {"type": "string", "description": "Root directory to search in. Defaults to '.'."},
},
"required": ["pattern"],
},
},
},
{
"type": "function",
"function": {
"name": "grep",
"description": "Search file contents for a regex pattern and return matching lines with file paths and line numbers.",
"parameters": {
"type": "object",
"properties": {
"pattern": {"type": "string", "description": "Regular expression to search for."},
"path": {"type": "string", "description": "Directory to search in. Defaults to '.'."},
"include": {"type": "string", "description": "Filename glob to restrict which files are searched (e.g. '*.py'). Defaults to '*'."},
},
"required": ["pattern"],
},
},
},
{
"type": "function",
"function": {
"name": "write_file",
"description": "Write content to a file, creating it (and any missing parent directories) if it does not exist.",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Path of the file to write."},
"content": {"type": "string", "description": "Full content to write to the file."},
},
"required": ["path", "content"],
},
},
},
{
"type": "function",
"function": {
"name": "edit_file",
"description": "Replace the first occurrence of a string in a file with a new string.",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "Path of the file to edit."},
"old_string": {"type": "string", "description": "Exact string to find and replace."},
"new_string": {"type": "string", "description": "String to replace it with."},
},
"required": ["path", "old_string", "new_string"],
},
},
},
{
"type": "function",
"function": {
"name": "webfetch",
"description": (
"Fetch a public URL (http/https only) and return its full plain-text content (up to 2 MB)."
),
"parameters": {
"type": "object",
"properties": {
"url": {"type": "string", "description": "The URL to fetch (http/https)."},
},
"required": ["url"],
},
},
},
{
"type": "function",
"function": {
"name": "todo_append",
"description": (
"Add a new item to the to-do list. "
"Use this to track a task you plan to work on."
),
"parameters": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Unique identifier for the item (e.g. '1', 'task-setup').",
},
"content": {
"type": "string",
"description": "Description of the task.",
},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "done", "cancelled", "failed"],
"description": "Initial status of the item. Use 'pending' for new tasks.",
},
},
"required": ["id", "content", "status"],
},
},
},
{
"type": "function",
"function": {
"name": "todo_list",
"description": (
"Read the current to-do list. "
"By default shows all active items (pending, in_progress, failed). "
"Set include_completed=true to also see done and cancelled items. "
"Failed items display their retry count."
),
"parameters": {
"type": "object",
"properties": {
"include_completed": {
"type": "boolean",
"description": "If true, include done and cancelled items in the output. Defaults to false.",
},
},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "todo_update",
"description": (
"Update the content or status of an existing to-do item. "
"At least one of 'content' or 'status' must be provided. "
"Setting a failed item back to in_progress counts as a retry and "
"is tracked automatically. The response will warn when the retry "
"limit is reached."
),
"parameters": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ID of the to-do item to update.",
},
"content": {
"type": "string",
"description": "New description for the item. Omit to leave unchanged.",
},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "done", "cancelled", "failed"],
"description": "New status for the item. Omit to leave unchanged.",
},
},
"required": ["id"],
},
},
},
{
"type": "function",
"function": {
"name": "read_scratchpad",
"description": (
"Read the current contents of the in-memory scratchpad. "
"Returns '(empty)' if nothing has been written yet."
),
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "write_scratchpad",
"description": (
"Overwrite the entire contents of the in-memory scratchpad with new content. "
"The previous content is permanently replaced."
),
"parameters": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The new content to store in the scratchpad.",
},
},
"required": ["content"],
},
},
},
{
"type": "function",
"function": {
"name": "ask_question",
"description": (
"Ask the user a clarifying question and wait for their answer. "
"Use this when you are missing information required to complete the task "
"and cannot reasonably infer it from context. "
"Ask one focused question at a time. "
"Do not use this for progress updates or to confirm actions you can already "
"take — only ask when you are genuinely blocked."
),
"parameters": {
"type": "object",
"properties": {
"question": {
"type": "string",
"description": "The question to ask the user.",
},
},
"required": ["question"],
},
},
},
]

View File

@@ -0,0 +1,31 @@
class Scratchpad:
"""Read and write from a in-memory scratchpad"""
def __init__(self):
self._content = ""
def read(self) -> str:
if self._content == "":
return "(empty)"
return self._content
def write(self, content: str) -> str:
self._content = str(content).strip()
return self._content
scratchpad = Scratchpad()
def read_scratchpad():
"""Read the contents of the scratchpad"""
return scratchpad.read()
def write_scratchpad(content: str):
"""
Write into the scratchpad. The previous content
will be overwritten.
"""
scratchpad.write(content)
return "Successfully written content into scratchpad"

View File

@@ -0,0 +1,12 @@
import subprocess
def run_bash(command: str) -> str:
"""Run a bash command and return its output."""
result = subprocess.run(
command, shell=True, text=True, capture_output=True
)
output = result.stdout
if result.stderr:
output += f"\nSTDERR:\n{result.stderr}"
return output or "(no output)"

View File

@@ -0,0 +1,116 @@
RETRY_LIMIT = 3
class ToDoList:
"""
Helper class to hold a to-do list in memory
"""
statuses = ["pending", "in_progress", "done", "cancelled", "failed"]
def __init__(self):
self._items = []
def read(self, include_completed=False):
"""Read the to-do list"""
if include_completed:
return [item.copy() for item in self._items]
else:
return [item.copy() for item in self._items
if item["status"] != "done" and item["status"] != "cancelled"]
def append(self, id, content, status):
if status not in ToDoList.statuses:
raise Exception(f"Invalid status {status}. "
"Valid to-do statuses: pending, in_progress, done, "
"cancelled, failed")
if self.contains(id):
raise Exception(f"To do item {id} already exists!")
new_item = {"id": id, "content": content,
"status": status, "retries": 0}
self._items.append(new_item)
return new_item.copy()
def contains(self, id) -> bool:
"""Check if the to do list contains an item with a specific id"""
for item in self._items:
if item["id"] == id:
return True
return False
def update(self, id, content, status):
if status is not None and status not in ToDoList.statuses:
raise Exception(f"Invalid status {status}. "
"Valid to-do statuses: pending, in_progress, done, "
"cancelled, failed")
idx = 0
while idx < len(self._items):
if self._items[idx]["id"] == id:
if content is not None:
self._items[idx]["content"] = content
if status is not None:
prev_status = self._items[idx]["status"]
self._items[idx]["status"] = status
# A failed task being set back to in_progress is a retry attempt.
if prev_status == "failed" and status == "in_progress":
self._items[idx]["retries"] += 1
return self._items[idx].copy()
idx += 1
raise Exception(f"To do item with id {id} not found")
todo_store = ToDoList()
def todo_append(id, content, status) -> str:
"""Append a new to do item to the to do list"""
id_str = str(id)
content_str = str(content)
status_str = str(status)
try:
todo_store.append(id_str, content_str, status_str)
return f"Successfully appended to do item {id_str} in to do list!"
except Exception as e:
return f"Failed to append to do item: {e}"
def todo_list(include_completed=False) -> str:
"""List all the items in the to do list"""
items = todo_store.read(include_completed)
result = f"To Do List ({len(items)} items)\n"
for status in ToDoList.statuses:
count = sum(1 for i in items if i["status"] == status)
result += f"{count} {status} items\n"
result += "-----\n"
for item in items:
retry_note = f", {item['retries']
} retries" if item["retries"] > 0 else ""
result += f"- [{item['id']}] {item['content']
} ({item['status']}{retry_note})\n"
return result
def todo_update(id, content=None, status=None) -> str:
if content is None and status is None:
return "No content or status was given to update. Nothing to do."
try:
item = todo_store.update(id, content, status)
retries = item["retries"]
if item["status"] == "in_progress" and retries > 0:
if retries >= RETRY_LIMIT:
return (
f"Updated to do item {id} to in_progress — "
f"but this is retry {retries} of {
RETRY_LIMIT} (retry limit reached). "
f"Do not retry again. Escalate to the user instead."
)
return (
f"Successfully updated to do item {id}! "
f"Retry attempt {retries} of {RETRY_LIMIT}."
)
return f"Successfully updated to do item {id}!"
except Exception as e:
return f"Failed to update to do item {id}: {e}"

View File

@@ -0,0 +1,41 @@
import re
import urllib.request
from urllib.parse import urlparse
from bs4 import BeautifulSoup
def webfetch(url: str) -> str:
"""Fetch a URL and return its full plain-text content (up to 2 MB)."""
try:
parsed = urlparse(url)
if parsed.scheme not in ("http", "https"):
return f"Error fetching {url}: unsupported scheme '{parsed.scheme}'. Only http and https are allowed."
max_bytes = 2 * 1024 * 1024
req = urllib.request.Request(url, headers={"User-Agent": "agent/1.0"})
with urllib.request.urlopen(req, timeout=15) as resp:
content_type = resp.headers.get_content_type()
if content_type and content_type not in (
"text/html",
"text/plain",
"application/xhtml+xml",
):
return f"Error fetching {url}: unsupported content type '{content_type}'."
charset = resp.headers.get_content_charset() or "utf-8"
raw_chunks = []
total = 0
while True:
chunk = resp.read(65536)
if not chunk:
break
total += len(chunk)
if total > max_bytes:
raw_chunks.append(chunk[: max_bytes - (total - len(chunk))])
break
raw_chunks.append(chunk)
raw = b"".join(raw_chunks).decode(charset, errors="replace")
soup = BeautifulSoup(raw, "html.parser")
text = soup.get_text(separator="\n", strip=True)
return re.sub(r"\n{3,}", "\n\n", text).strip()
except Exception as e:
return f"Error fetching {url}: {e}"