Files
Agentswarm/orchestrator/result_aggregator.py
T
gongzhiyongandClaude Opus 4.8 fb1eeeebfa fix(swebench): 真实仓 patch 捕获 + agent 稳定性
#1 稳定性:HEARTBEAT_TIMEOUT 30→120(可配 AGENT_HEARTBEAT_TIMEOUT)——真实仓 clone/长
模型调用不再误杀 agent。
#2 正确性/忠实度:
- 透传 GIT_BASE_COMMIT;agent clone 改为浅 fetch+checkout 该 commit(在正确基线上读/改,
  且大仓也快);emit_patch 持久化 base_commit + _git_diff 对 SHA 走浅 fetch。
- agent 读文件预算可配并调大(AGENT_CTX_MAX_FILES=40/BYTES=40000),LLM 看到完整文件,
  避免全量重写截断 → diff 失真。launcher 透传这些 env 给 agent pod。

import + runtime-contract/result-aggregator/security-boundary 测试全绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 00:25:38 +08:00

402 lines
19 KiB
Python

"""ResultAggregator — 蜂群的终态聚合节点(对应掘金文章的"报告生成 Agent",mode=wait_all)。
设计依据 https://juejin.cn/post/7603575399255949352 的协作聚合模型,取代旧的"蜂后 best-of-N 选优":
- 中间产物存**共享结果池**(task.result.files,经 collect_generated_files 读取),**不进 git 交付仓**;
- run 终态(所有子任务完成)→ 从共享池**收集所有 agent 的互补产出**;
- 同一文件被多个 agent 各写一部分时,**整合成一份完整产物**:优先 LLM(协作合并),无 LLM 时用
**AST 函数级合并**(提取各版本 def/class/import 拼合,而非"取最长版本"糊弄);
- 跑测试**验证**(沙箱,fail-closed);
- 不达标且未到轮次上限 → **打回迭代**(decision=bounce,调用方 reopen 重做);
- 达标/达上限 → **单次**把整合产物 push 到 main —— 产物仓永远只有 main 一份,无 agent 工作分支。
不 import orchestrator.main(避免循环);finalize_run 返回 plain dict,调用方折进 deliverable 并据
decision 决定是否打回。质量驱动闭环 = 聚合合并 → 验证 → 不过打回 → 达标落库。
"""
from __future__ import annotations
import ast
import asyncio
import logging
import os
import re
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
try:
from openai import AsyncOpenAI
except Exception: # pragma: no cover
AsyncOpenAI = None
def _aggregator_timeout() -> float:
try:
return float(os.getenv("AGGREGATOR_TIMEOUT_SECONDS", "90") or 90)
except ValueError:
return 90.0
def _llm():
"""(client, model) for LLM file integration, or (None, model) when no key — degrades to AST merge."""
api_key = os.getenv("OPENAI_API_KEY") or os.getenv("MODEL_API_KEY")
api_base = (os.getenv("OPENAI_API_BASE") or os.getenv("MODEL_API_BASE")
or "https://api.openai.com/v1")
model = (os.getenv("OPENAI_MODEL") or os.getenv("MODEL_NAME") or os.getenv("MODEL_ID")
or "gpt-4o-mini")
model = os.getenv("MASTER_REVIEW_MODEL", model)
client = AsyncOpenAI(api_key=api_key, base_url=api_base) if (api_key and AsyncOpenAI) else None
return client, model
_FENCE = re.compile(r"^```[a-zA-Z0-9_+-]*\n(.*?)\n```$", re.S)
def _strip_fence(text: str) -> str:
m = _FENCE.match(text.strip())
return m.group(1) if m else text.strip()
def _ast_merge_python(contents: List[str]) -> Optional[str]:
"""确定性整合多个 Python 版本:提取各版本的 import 与顶层 def/class,按名字去重合并(同名后者覆盖)。
比"取最长版本"强 —— 真把各 agent 写的不同函数拼成一份完整文件。解析全失败 → None(回退最长)。"""
imports: Dict[str, str] = {}
defs: Dict[str, str] = {}
parsed_any = False
for content in contents:
try:
tree = ast.parse(content)
except SyntaxError:
continue
parsed_any = True
for node in tree.body:
seg = ast.get_source_segment(content, node)
if not seg:
continue
if isinstance(node, (ast.Import, ast.ImportFrom)):
imports[seg.strip()] = seg
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
defs[node.name] = seg
if not parsed_any or not defs:
return None
parts = list(imports.values())
if parts:
parts.append("")
parts.extend(defs.values())
return "\n".join(parts).strip() + "\n"
def _merge_fallback(path: str, contents: List[str]) -> str:
"""无 LLM 兜底:Python 走 AST 函数级合并,其他文件取信息量最大版本。"""
if path.endswith(".py"):
merged = _ast_merge_python(contents)
if merged:
return merged
return max(contents, key=len)
# ---------- 收集共享池 ----------
def _collect_per_path(tasks) -> Dict[str, List[Any]]:
"""从所有子任务的共享池产出,按文件 path 分组保留**每个版本**(不去重 —— 去重会丢互补内容)。
返回 {path: [SandboxFile, ...]}。复用 quality.collect_generated_files 逐 task 提取(它已兼容
task.result.files 与旧 subtasks 两种结构)。"""
from .quality import collect_generated_files
by_path: Dict[str, List[Any]] = {}
for t in tasks:
cf = collect_generated_files([t])
for kind in ("impl", "agent_tests"):
for f in cf.get(kind) or []:
by_path.setdefault(f.path, []).append(f)
return by_path
# ---------- 同文件互补整合(协作合并) ----------
async def _merge_one_path(path: str, versions: List[Any], objective: str) -> Any:
"""同一文件多个 agent 版本 → 整合成一份完整产物。
单版本直接用;多版本优先 LLM 整合(文章的聚合节点),失败/无 LLM 时用 AST 函数级合并兜底。"""
from .sandbox import SandboxFile
contents = [v.content for v in versions]
if len(contents) == 1:
return versions[0]
client, model = _llm()
if not client:
logger.warning("aggregator: no LLM for %s; AST/longest merge of %d versions", path, len(contents))
return SandboxFile(path=path, content=_merge_fallback(path, contents))
try:
joined = "\n\n".join(f"===VERSION {i}===\n{c}" for i, c in enumerate(contents))
resp = await client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": (
"你是蜂群的结果聚合节点。多个 agent 各自实现了同一个文件的不同部分。"
"请把它们整合成一个完整、正确、无重复、可直接运行的文件:合并所有互补的函数/类/导入,"
"去掉重复定义,保持一致风格。只输出该文件的最终完整内容,不要任何解释、不要 markdown 围栏。")},
{"role": "user", "content": (
f"目标:{objective}\n文件路径:{path}\n\n"
f"以下是 {len(contents)} 个 agent 各自的版本:\n\n{joined}")},
],
timeout=_aggregator_timeout(),
)
out = _strip_fence((resp.choices[0].message.content or "").strip())
return SandboxFile(path=path, content=out or _merge_fallback(path, contents))
except Exception as exc:
logger.warning("aggregator: LLM merge failed for %s (%s); AST/longest fallback", path, exc)
return SandboxFile(path=path, content=_merge_fallback(path, contents))
async def merge_artifacts(tasks, objective: str) -> List[Any]:
"""收集共享池 → 对每个文件整合互补产出 → 返回完整产物文件集(SandboxFile 列表)。"""
by_path = _collect_per_path(tasks)
merged: List[Any] = []
for path, versions in by_path.items():
merged.append(await _merge_one_path(path, versions, objective))
return merged
# ---------- 验证(沙箱,fail-closed) ----------
async def _validate(merged: List[Any]) -> Dict[str, Any]:
"""跑测试验证整合后的产物。fail-closed:隔离未确认/无测试 → 不评分(诚实 None,非 0)。"""
from .sandbox import run_tests, isolation_confirmed
from .quality import _is_test_file
if not isolation_confirmed():
return {"validated": False, "reason": "sandbox_not_isolated", "pass_rate": None}
impl = [f for f in merged if not _is_test_file(f.path)]
tests = [f for f in merged if _is_test_file(f.path)]
if not tests:
return {"validated": False, "reason": "no_tests", "pass_rate": None}
try:
res = await asyncio.to_thread(run_tests, impl, tests)
return {"validated": True, "pass_rate": res.pass_rate,
"passed": res.passed, "total": res.total}
except Exception as exc:
return {"validated": False, "reason": f"error:{exc!r}", "pass_rate": None}
# ---------- 落 main(单次,复用 git helper) ----------
def _auth_url(repo_url: str, user: Optional[str], pw: Optional[str]) -> str:
"""凭据嵌入 http(s) clone URL,URL-encode 密码。结果**绝不记日志**。"""
import urllib.parse
if not user or not pw or "://" not in repo_url:
return repo_url
scheme, rest = repo_url.split("://", 1)
return f"{scheme}://{urllib.parse.quote(user, safe='')}:{urllib.parse.quote(pw, safe='')}@{rest}"
def _git_push(env: Dict[str, str], base_branch: str, files: List[Any], swarm_id: str) -> Dict[str, Any]:
"""同步 git:clone base_branch → 写入整合产物 → commit → push。阻塞,经 to_thread 调用。
凭据只活在 clone URL,绝不记日志。"""
import shutil
import subprocess
import tempfile
repo_url = env["GIT_REPO_URL"]
auth_url = _auth_url(repo_url, env.get("GIT_USERNAME"), env.get("GIT_PASSWORD"))
workdir = tempfile.mkdtemp(prefix="aggregate-promote-")
repo_dir = os.path.join(workdir, "repo")
def git(*args, cwd=None):
return subprocess.run(["git", *args], cwd=cwd or workdir,
capture_output=True, text=True, timeout=180)
try:
if git("clone", "--depth", "1", "--branch", base_branch, auth_url, "repo").returncode != 0:
return {"promoted": False, "reason": "clone_failed"}
for f in files:
dest = os.path.join(repo_dir, f.path)
os.makedirs(os.path.dirname(dest) or repo_dir, exist_ok=True)
with open(dest, "w", encoding="utf-8") as fh:
fh.write(f.content)
git("config", "user.email", "aggregator@heicode.swarm", cwd=repo_dir)
git("config", "user.name", "HeiCode Aggregator", cwd=repo_dir)
git("add", "-A", cwd=repo_dir)
c = git("commit", "-m", f"aggregate: integrated swarm deliverable ({swarm_id})", cwd=repo_dir)
if c.returncode != 0:
return {"promoted": False, "reason": "no_changes"}
if git("push", "origin", base_branch, cwd=repo_dir).returncode != 0:
return {"promoted": False, "reason": "push_failed"}
sha = git("rev-parse", "HEAD", cwd=repo_dir).stdout.strip()
return {"promoted": True, "commit_sha": sha, "branch": base_branch}
except Exception as exc:
return {"promoted": False, "reason": f"error:{exc!r}"}
finally:
shutil.rmtree(workdir, ignore_errors=True)
def _emit_patch_mode(run) -> bool:
"""Benchmark 模式(SWE-bench 等):产出 unified diff,不 push 到 main。
per-run 门控(run.metadata['emit_patch'])或 per-deployment 门控(env SWARM_EMIT_PATCH)。
默认关 —— 生产路径仍走 _promote,行为不变。"""
if (run.metadata or {}).get("emit_patch"):
return True
return os.getenv("SWARM_EMIT_PATCH", "").strip().lower() in {"1", "true", "yes"}
def _git_diff(env: Dict[str, str], base_ref: str, files: List[Any], swarm_id: str) -> Dict[str, Any]:
"""_git_push 的 benchmark 变体:clone → checkout base_ref → 写入整合产物 → `git add -A` →
`git diff --cached`(相对 base 的 unified diff)。**只产 diff,不 commit、不 push**。
凭据只活在 clone URL,绝不记日志;diff 本身只含代码,无凭据。阻塞,经 to_thread 调用。"""
import shutil
import subprocess
import tempfile
repo_url = env["GIT_REPO_URL"]
auth_url = _auth_url(repo_url, env.get("GIT_USERNAME"), env.get("GIT_PASSWORD"))
workdir = tempfile.mkdtemp(prefix="aggregate-patch-")
repo_dir = os.path.join(workdir, "repo")
def git(*args, cwd=None):
return subprocess.run(["git", *args], cwd=cwd or workdir,
capture_output=True, text=True, timeout=600)
# Heuristic: a 40-hex (or short-hex) ref is a commit SHA → shallow-fetch just it (fast/small even
# for huge repos). A branch name → shallow clone that branch. Falls back to full clone on failure.
def _looks_like_sha(ref: str) -> bool:
return bool(ref) and len(ref) >= 7 and all(c in "0123456789abcdef" for c in ref.lower())
try:
if base_ref and _looks_like_sha(base_ref):
os.makedirs(repo_dir, exist_ok=True)
ok = (git("init", "-q", "repo").returncode == 0
and git("remote", "add", "origin", auth_url, cwd=repo_dir).returncode == 0
and git("fetch", "--depth", "1", "origin", base_ref, cwd=repo_dir).returncode == 0
and git("checkout", "-q", "FETCH_HEAD", cwd=repo_dir).returncode == 0)
if not ok:
return {"emitted": False, "reason": "shallow_checkout_failed"}
else:
if git("clone", auth_url, "repo").returncode != 0:
return {"emitted": False, "reason": "clone_failed"}
if base_ref and git("checkout", base_ref, cwd=repo_dir).returncode != 0:
return {"emitted": False, "reason": "checkout_failed"}
for f in files:
dest = os.path.join(repo_dir, f.path)
os.makedirs(os.path.dirname(dest) or repo_dir, exist_ok=True)
with open(dest, "w", encoding="utf-8") as fh:
fh.write(f.content)
git("add", "-A", cwd=repo_dir)
d = git("diff", "--cached", cwd=repo_dir)
if d.returncode != 0:
return {"emitted": False, "reason": "diff_failed"}
if not (d.stdout or "").strip():
return {"emitted": False, "reason": "empty_patch"}
return {"emitted": True, "patch": d.stdout, "base_ref": base_ref}
except Exception as exc:
return {"emitted": False, "reason": f"error:{exc!r}"}
finally:
shutil.rmtree(workdir, ignore_errors=True)
async def _emit_patch(run, files: List[Any]) -> Dict[str, Any]:
"""Benchmark 入口:把整合产物相对 base_commit 产成 unified diff(SWE-bench patch 格式)。
凭据按需从 grant.secret_ref 解析,绝不存储/记录。base_ref 优先 grant.base_commit(SWE-bench
精确 commit),回退 base_branch。"""
try:
grant = (run.metadata or {}).get("git_grant") or {}
repo_url = grant.get("repo_url")
if not repo_url:
return {"emitted": False, "reason": "no_git_grant"}
if not files:
return {"emitted": False, "reason": "no_files"}
from .agent_launcher import resolve_git_grant
env = resolve_git_grant({"resource_grants": [{
"resource_type": "git", "secret_ref": grant.get("secret_ref"),
"metadata": {"repo_url": repo_url, "base_branch": grant.get("base_branch", "main")},
}]})
if not env or "GIT_REPO_URL" not in env:
return {"emitted": False, "reason": "grant_unresolved"}
base_ref = grant.get("base_commit") or grant.get("base_branch", "main")
return await asyncio.to_thread(_git_diff, env, base_ref, files, getattr(run, "swarm_id", "?"))
except Exception as exc:
logger.warning("aggregator: emit_patch failed for %s: %s", getattr(run, "swarm_id", "?"), exc)
return {"emitted": False, "reason": f"error:{exc!r}"}
async def _promote(run, files: List[Any]) -> Dict[str, Any]:
"""把整合产物 push 到产物仓 base 分支(main)。凭据按需从 grant.secret_ref 解析,绝不存储/记录。"""
try:
grant = (run.metadata or {}).get("git_grant") or {}
repo_url = grant.get("repo_url")
if not repo_url:
return {"promoted": False, "reason": "no_git_grant"}
if not files:
return {"promoted": False, "reason": "no_files"}
from .agent_launcher import resolve_git_grant
env = resolve_git_grant({"resource_grants": [{
"resource_type": "git", "secret_ref": grant.get("secret_ref"),
"metadata": {"repo_url": repo_url, "base_branch": grant.get("base_branch", "main")},
}]})
if not env or "GIT_REPO_URL" not in env:
return {"promoted": False, "reason": "grant_unresolved"}
return await asyncio.to_thread(_git_push, env, grant.get("base_branch", "main"),
files, getattr(run, "swarm_id", "?"))
except Exception as exc:
logger.warning("aggregator: promote failed for %s: %s", getattr(run, "swarm_id", "?"), exc)
return {"promoted": False, "reason": f"error:{exc!r}"}
# ---------- 质量决策 ----------
def _decide(validation: Dict[str, Any], threshold: Optional[float],
cycles: int, max_cycles: int) -> str:
"""聚合质量门决策:accept(达标/无门/无法评分/达上限)或 bounce(评分不达标且未到上限)。
诚实(规则 #9):无法评分(pass_rate=None)不打回 —— 不在"算不出的分"上重做。"""
if threshold is None:
return "accept"
pass_rate = validation.get("pass_rate")
if pass_rate is None:
return "accept"
if pass_rate >= threshold:
return "accept"
if cycles >= max_cycles:
return "accept" # 达上限:接受当前最好的,交收敛标 MAX_ROUNDS_REACHED
return "bounce"
# ---------- 终态入口 ----------
async def finalize_run(run, tasks, objective: str = "", threshold: Optional[float] = None,
cycles: int = 0, max_cycles: int = 2) -> Dict[str, Any]:
"""聚合节点入口(run 终态调用,此时子任务已 wait_all 完成)。
收集共享池 → 整合互补产出 → 验证 → 决策。
- decision='bounce':不 promote,调用方 reopen 重做(质量驱动迭代);
- decision='accept':单次落 main。
Best-effort,绝不让终态路径崩。"""
try:
merged = await merge_artifacts(tasks, objective or "")
if not merged:
return {"finalized": False, "decision": "accept", "reason": "no_artifacts",
"merged_file_count": 0}
validation = await _validate(merged)
decision = _decide(validation, threshold, cycles, max_cycles)
if decision == "bounce":
return {"finalized": False, "decision": "bounce", "validation": validation,
"merged_file_count": len(merged), "files": [f.path for f in merged]}
# Benchmark 模式:产 unified diff 不 push(SWE-bench 等需要 patch,产物仓不能被改);
# 生产模式:单次落 main。两条互斥,由 _emit_patch_mode 门控。
if _emit_patch_mode(run):
patch_res = await _emit_patch(run, merged)
return {
"finalized": True,
"decision": "accept",
"mode": "emit_patch",
"merged_file_count": len(merged),
"files": [f.path for f in merged],
"validation": validation,
"patch": patch_res.get("patch"),
"patch_meta": {k: v for k, v in patch_res.items() if k != "patch"},
}
promotion = await _promote(run, merged)
return {
"finalized": True,
"decision": "accept",
"merged_file_count": len(merged),
"files": [f.path for f in merged],
"validation": validation,
"promotion": promotion,
}
except Exception as exc:
logger.warning("aggregator: finalize_run failed for %s: %s",
getattr(run, "swarm_id", "?"), exc)
return {"finalized": False, "decision": "accept", "reason": f"error:{exc!r}",
"merged_file_count": 0}