diff --git a/agent/task_executor.py b/agent/task_executor.py index 979f86f..383217c 100644 --- a/agent/task_executor.py +++ b/agent/task_executor.py @@ -11,6 +11,7 @@ import asyncio import json import logging import os +import re import time from pathlib import Path from typing import Callable, Optional @@ -377,6 +378,7 @@ Return ONLY the JSON array, no other text.""" task_id, context.get("specialist_role", "general"), description) workspace_files = self._summarize_workspace() workspace_context = self._collect_workspace_context() + exact_target_files = self._load_exact_target_files(description) user_prompt = context.get("user_prompt") or context.get("run_goal") or context.get("root_task_description") or "" specialist_role = context.get("specialist_role", "general") dependency_artifacts = context.get("dependency_artifacts") or [] @@ -447,6 +449,16 @@ Current workspace files: {json.dumps(workspace_files, indent=2)} Relevant workspace file contents: {json.dumps(workspace_context, indent=2)} + +EXACT CURRENT CONTENT of the file(s) this task edits (verbatim from disk — authoritative): +{json.dumps(exact_target_files, indent=2)} +^ When a file appears above, your returned `content` for it MUST be that exact text with ONLY the + minimal edits the task requires. Copy every other line byte-for-byte: imports, comments, copyright + headers, blank lines, whitespace, and unrelated code. Do NOT reformat, reorder, rename, restyle, + "clean up", or re-wrap anything the task does not explicitly require. The resulting diff against the + base commit must contain ONLY the lines the task needs — unrelated changes break tests and fail the + patch. If a file you must edit is NOT shown above, read it conceptually from the listed workspace + contents and still change as little as possible. Peer specialist input: {json.dumps(peer_context, indent=2)} @@ -457,7 +469,9 @@ Workspace & tool boundaries (IMPORTANT): it is not listed above: if the task references e.g. `qutebrowser/utils/log.py`, that file EXISTS on disk at {self.workspace_dir}/qutebrowser/utils/log.py — assume it is present and edit it. - Treat the local files as the single source of truth. Return the complete modified content of each - file you change in `files` (full file, not a diff). + file you change in `files` (full file, not a diff) — but it MUST be the file's exact current content + with only the minimal task-required edits applied (see "EXACT CURRENT CONTENT" above). Preserve all + unchanged lines verbatim; never regenerate a file from memory or reformat it. - The web search / read_url tools are for EXTERNAL knowledge ONLY (library/framework docs, language features, error-message lookups). NEVER use them to fetch THIS repository's own source — the web copy is a DIFFERENT version than your local base commit and will corrupt your patch. @@ -852,6 +866,48 @@ Return ONLY JSON: logger.warning(f"Failed to read workspace file {relative}: {e}") return context + #: Path-like tokens in a subtask description, e.g. `lib/ansible/module_utils/urls.py`. + _PATH_RE = re.compile( + r"[A-Za-z0-9_][A-Za-z0-9_./\-]*\.(?:py|js|jsx|ts|tsx|go|rb|java|c|h|cc|cpp|hpp|" + r"rs|php|cs|kt|swift|scala|vue|json|ya?ml|toml|cfg|ini|sh)\b" + ) + + def _load_exact_target_files(self, description: str, max_files: int = 6, + max_bytes: int = 120000) -> dict[str, str]: + """Load the FULL, UNTRUNCATED on-disk content of the file(s) a subtask names. + + Root cause of noisy/non-applying patches: the agent was only shown a truncated SAMPLE of the + repo, so it reconstructed whole files from partial input → dropped/reordered/reformatted lines + → huge diffs that break tests. Here we read the exact current content of the files referenced + in the subtask description so the model can preserve every unchanged line verbatim and edit + only what the task requires. Paths that don't resolve inside the workspace are skipped.""" + if not description: + return {} + out: dict[str, str] = {} + # Longest paths first: prefer `a/b/c.py` over a bare `c.py` mention of the same file. + cands = sorted(set(m.group(0) for m in self._PATH_RE.finditer(description)), + key=len, reverse=True) + for cand in cands: + if len(out) >= max_files: + break + try: + p = self._resolve_workspace_path(cand) + except ValueError: + continue + if not p.is_file(): + continue + try: + data = p.read_bytes() + except Exception: + continue + if len(data) > max_bytes: + continue # too large to inject in full; leave to the sampled context + try: + out[cand] = data.decode("utf-8") + except UnicodeDecodeError: + continue + return out + def _resolve_workspace_path(self, file_path: str) -> Path: if not file_path or os.path.isabs(file_path): raise ValueError(f"Invalid relative path: {file_path}")