Author SHA1 Message Date
xiaohei 983518522c 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
2026-06-22 09:49:04 +00:00
xiaoheiandClaude Opus 4.8 435f720cfb fix(agent): 注入目标文件完整原文 + 强令最小改动(修整文件重写噪声)
CI / guardrails (push) Successful in 5s
CI / guardrails (pull_request) Successful in 7s
CI / tests (push) Successful in 48s
CI / tests (pull_request) Successful in 41s
根因: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>
2026-06-22 09:47:35 +00:00
xiaohei 85e6ea6dfb Merge pull request 'fix: 关 qwen 思考模式(消除 reasoning 回退) + 修 bounce 死锁 (BUG-A/BUG-B)' (#26) from fix/qwen-thinking-and-bounce-deadlock into main
CI / guardrails (push) Successful in 5s
CI / tests (push) Successful in 48s
Reviewed-on: #26
2026-06-22 05:55:05 +00:00
xiaoheiandClaude Opus 4.8 cdb17a0d73 fix(agent+orchestrator): 关 qwen 思考模式出干净 JSON + 修 bounce 死锁
CI / guardrails (push) Successful in 6s
CI / tests (push) Successful in 45s
CI / guardrails (pull_request) Successful in 8s
CI / tests (pull_request) Successful in 43s
BUG-A: qwen3.7-max 默认思考模式,重 impl 子任务把预算花在 reasoning_content
上不出最终 content JSON → json.loads("") 失败。task_executor._complete 两处
chat.completions.create 注入 extra_body={"enable_thinking": False}(env
AGENT_ENABLE_THINKING=1 可opt-in)。对齐 Qwen 官方结构化输出用法。

BUG-B: bounce 仅重开含 impl 文件的任务;impl 失败(0文件)+test成功时
reopens=0 → run 永远 running 空转。加兜底:reopens==0 时重开 FAILED 任务
(受 MAX_REVIEW_CYCLES 约束);仍无可重开则 accept_no_rework 走正常收敛终结。

契约测试全绿(runtime-contract/merge-smoke/workflow-e2e/contract-freeze)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 05:44:00 +00:00
xiaohei b0831645cf Merge pull request 'Feat/queen convergence' (#24) from feat/queen-convergence into main
CI / guardrails (push) Successful in 6s
CI / tests (push) Successful in 41s
Reviewed-on: #24
2026-06-21 20:00:36 +00:00
xiaohei 5cd266ae17 Merge pull request 'Feat/queen convergence' (#23) from feat/queen-convergence into main
CI / guardrails (push) Successful in 7s
CI / tests (push) Successful in 46s
Reviewed-on: #23
2026-06-21 17:47:43 +00:00
xiaohei fadca8f4fc Merge pull request '蜂后收敛闭环 P0+M2-M4(防抢run/选最优/质量门打回/失败隔离)' (#19) from feat/queen-convergence into main
CI / tests (push) Successful in 39s
CI / guardrails (push) Failing after 13m24s
Reviewed-on: #19
2026-06-19 13:57:12 +00:00
2 changed files with 96 additions and 9 deletions
+73 -2
View File
@@ -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.
@@ -610,9 +624,19 @@ Return ONLY the JSON, no other text."""
rc = extra.get("reasoning_content") if isinstance(extra, dict) else None
return (rc or "").strip()
@staticmethod
def _thinking_extra_body() -> Optional[dict]:
"""qwen3.7-max defaults to THINKING mode: on heavy impl subtasks it spends the token
budget on `reasoning_content` and never emits the final `content` JSON, so json.loads("")
fails ("Expecting value: line 1 column 1"). Disable thinking for deterministic structured
execution. Set AGENT_ENABLE_THINKING=1 to opt back in (e.g. for non-structured analysis)."""
enabled = os.getenv("AGENT_ENABLE_THINKING", "false").strip().lower() in ("1", "true", "yes", "on")
return None if enabled else {"enable_thinking": False}
async def _complete(self, prompt: str, max_tokens: int) -> str:
"""Call the LLM with optional Jina MCP tools; handles the tool-call loop."""
extra_headers = self._model_attribution_headers()
extra_body = self._thinking_extra_body()
tools = await self._load_jina_tools()
messages = [{"role": "user", "content": prompt}]
@@ -623,6 +647,8 @@ Return ONLY the JSON, no other text."""
max_tokens=max_tokens,
extra_headers=extra_headers or None,
)
if extra_body:
kwargs["extra_body"] = extra_body
if tools:
kwargs["tools"] = tools
kwargs["tool_choice"] = "auto"
@@ -656,10 +682,13 @@ Return ONLY the JSON, no other text."""
# Fallback: ask for a final answer without tools
messages.append({"role": "user", "content": "Please provide your final answer now."})
response = await self.client.chat.completions.create(
fallback_kwargs: dict = dict(
model=self.model, messages=messages, max_tokens=max_tokens,
extra_headers=extra_headers or None,
)
if extra_body:
fallback_kwargs["extra_body"] = extra_body
response = await self.client.chat.completions.create(**fallback_kwargs)
self._record_openai_usage(response)
return self._message_text(response.choices[0].message)
@@ -837,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}")
+16
View File
@@ -776,6 +776,14 @@ async def refresh_swarm_run_status(run):
if collect_generated_files([t]).get("impl"):
if await task_queue.reopen_task(t.task_id):
reopened += 1
# Deadlock guard: when the impl subtask(s) FAILED (0 impl files) and only test tasks
# produced output, the loop above finds nothing to reopen → reopens=0 → the run would
# spin 'running' forever. Reopen the FAILED tasks so the next cycle retries the impl.
if reopened == 0:
for t in tasks:
if t.status == TaskStatus.FAILED and await task_queue.reopen_task(t.task_id):
reopened += 1
if reopened > 0:
run.metadata["review_cycles"] = agg_cycles + 1
run.metadata["rework_reopens"] = int(run.metadata.get("rework_reopens", 0) or 0) + reopened
await swarm_runtime.save_run(run)
@@ -783,6 +791,14 @@ async def refresh_swarm_run_status(run):
run.swarm_id, reopened, agg_cycles + 1,
(agg.get("validation") or {}).get("pass_rate"))
return
# Nothing reopenable (no impl files and no failed tasks to retry): bouncing here would
# deadlock the run in 'running'. Stop bouncing and accept the existing artifacts so the
# run terminates via the normal completion/convergence path below.
agg["decision"] = "accept_no_rework"
run.metadata["aggregation"] = agg
logger.warning("aggregation: bounce on run %s but nothing reopenable (reopens=0) — "
"accepting existing artifacts to avoid deadlock (cycle %d)",
run.swarm_id, agg_cycles + 1)
await swarm_runtime.save_run(run)
if run.status == next_status: