fix(agent): 注入目标文件完整原文 + 强令最小改动(修整文件重写噪声)
根因:agent 无本地读文件工具,只拿到 workspace 的截断采样,被要求"输出完整文件" 时只能凭残缺采样+自身知识重构整个文件 → 丢行/重排/改注释/版权头 → 巨噪声 diff, 要么 apply 不上,要么 apply 了破坏无关测试(SWE-bench Pro 两臂 0/10 的主因)。 修法: - _load_exact_target_files():从子任务描述正则提取目标文件路径,注入其**完整未截断** 的磁盘原文(上限6文件/各120KB,超限留给采样)。 - 执行 prompt 新增 "EXACT CURRENT CONTENT" 段 + 强令:返回内容必须是该原文逐字节保留、 仅施加任务所需最小编辑,禁止 reformat/reorder/rename/clean-up 无关行。 - 强化原 full-file 输出说明。 目的:让 diff 只含必要行,救回 NodeBB 296/300、ansible 171/175 这类"差几个测试"的近失题。 契约测试全绿(runtime-contract/merge-smoke/workflow-e2e/contract-freeze)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
85e6ea6dfb
commit
435f720cfb
+57
-1
@@ -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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user