Merge pull request 'fix(agent): 最小改动产 patch — 注入目标文件完整原文,消除整文件重写噪声' (#27) from fix/minimal-diff-execution into main
CI / guardrails (push) Successful in 11s
CI / tests (push) Successful in 41s

Reviewed-on: #27
This commit was merged in pull request #27.
This commit is contained in:
2026-06-22 09:49:04 +00:00
+57 -1
View File
@@ -11,6 +11,7 @@ import asyncio
import json import json
import logging import logging
import os import os
import re
import time import time
from pathlib import Path from pathlib import Path
from typing import Callable, Optional 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) task_id, context.get("specialist_role", "general"), description)
workspace_files = self._summarize_workspace() workspace_files = self._summarize_workspace()
workspace_context = self._collect_workspace_context() 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 "" 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") specialist_role = context.get("specialist_role", "general")
dependency_artifacts = context.get("dependency_artifacts") or [] dependency_artifacts = context.get("dependency_artifacts") or []
@@ -447,6 +449,16 @@ Current workspace files:
{json.dumps(workspace_files, indent=2)} {json.dumps(workspace_files, indent=2)}
Relevant workspace file contents: Relevant workspace file contents:
{json.dumps(workspace_context, indent=2)} {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: Peer specialist input:
{json.dumps(peer_context, indent=2)} {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 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. 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 - 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 - 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 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. 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}") logger.warning(f"Failed to read workspace file {relative}: {e}")
return context 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: def _resolve_workspace_path(self, file_path: str) -> Path:
if not file_path or os.path.isabs(file_path): if not file_path or os.path.isabs(file_path):
raise ValueError(f"Invalid relative path: {file_path}") raise ValueError(f"Invalid relative path: {file_path}")