在去中心化重构之上落地 benchmark 对比管线:5 个系统(single/strong/chain/sub_agent/swarm) 跑同一任务集、同一执行后端,产出统一 BenchmarkRunRecord → 评估器算 G_E/G_E,c → 报告 + 回放。 - benchmark/runners/:backend(Offline 确定性 / OpenAI 真实)+ base + 5 个 runner。各 runner 用 held-out fixture 测试在 Group B 沙箱里评分得 TestPassRate(权威,非自评)。 - benchmark/tasksets/:统一任务集 + 加载器(coding-set-1,1 个 fixture)。 - benchmark/reports/、benchmark/replay/:G_E/G_E,c/coverage/confidence + 归档。 - benchmark/baselines/comparison.py:BenchmarkRunRecord 的 CodeReview/UserAcceptance 改为 Optional(掩码归一,未采集即 None,规则 #9)。 - scripts/run-benchmark-suite.py harness + scripts/test-benchmark-runners.py。 与去中心化重构对齐:swarm runner 拓扑已**重指向去中心化流程**(种子→自选→自主分解→竞争→ 同伴交叉评审→收敛,calls=6/review=1),非旧 Master「分解→派发→单评审」。仍用同一离线后端 建模以保证公平对比(驱动活体编排器会换后端→记录不可比;活体全流程由 test-workflow-e2e 验证)。 沙箱适配:runner 评分走 fail-closed 沙箱(#24),故 test + CI 步骤设 HEICODE_SANDBOX_ISOLATED=1 (仅 CI/隔离 Pod)。 影响范围:agent_swarm(benchmark 层 + 测试 + docs + CI)。不碰 orchestrator 编排逻辑、 不改 Manager↔Swarm 契约、不影响 Client/计费/密钥/审计/发布链路。 诚实边界: - **离线后端只验证管线**:所有系统拿同一参考解 → quality 相同 → G_E=0、swarm_valid=False, 刻意不显示蜂群优势(反造假)。真实 G_E>0 需 --backend openai + 足量冻结任务集 + 多次运行。 - 故 Closes #21(运行器 + 统一记录已落地并产出合规非 NaN 记录);Refs #20(仅 1/5 场景)、 Refs #22(评估器/报告/回放已建,但 Quality 仅 TestPassRate,CodeReview/UserAcceptance 缺)、 Refs #13(验收 EPIC,需真实 run 证明 Swarm>baselines,未满足)。 Closes #21 Refs #20 Refs #22 Refs #13 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
116 lines
4.7 KiB
Python
116 lines
4.7 KiB
Python
"""Execution backends for benchmark runners — the SHARED model gateway all systems run through.
|
|
|
|
Fairness (standard §9.1): swarm and every baseline must use the same backend on the same task
|
|
set; only the topology (call pattern) differs. Two backends:
|
|
|
|
- OfflineBackend: deterministic, no network, no key. Echoes the fixture's offline reference
|
|
solution and charges a notional per-call cost. Used for hermetic CI + pipeline validation.
|
|
It is IDENTICAL across systems, so offline runs can prove the pipeline (records → G_E →
|
|
report, no NaN) but CANNOT show a swarm quality advantage — by design (anti-fabrication).
|
|
|
|
- OpenAIBackend: real OpenAI-compatible generation (needs OPENAI_API_KEY / OPENAI_BASE_URL).
|
|
Produces real files + real token/cost. This is what yields REAL G_E numbers.
|
|
|
|
`generate()` returns the produced implementation files plus measured token/cost/time.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import re
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from typing import List, Optional, Tuple
|
|
|
|
# (path, content) pairs.
|
|
Files = List[Tuple[str, str]]
|
|
|
|
|
|
@dataclass
|
|
class GenerationResult:
|
|
files: Files = field(default_factory=list)
|
|
tokens: int = 0
|
|
cost_usd: float = 0.0
|
|
elapsed_s: float = 0.0
|
|
|
|
|
|
class ExecutionBackend(ABC):
|
|
name: str = "abstract"
|
|
|
|
@abstractmethod
|
|
def generate(self, *, objective: str, reference_files: Files, max_tokens: int) -> GenerationResult:
|
|
"""Produce implementation files for one objective."""
|
|
|
|
|
|
class OfflineBackend(ExecutionBackend):
|
|
"""Deterministic, key-free. Echoes the fixture reference solution; charges a notional cost.
|
|
|
|
cost_per_call / tokens_per_call / time_per_call let the harness model a 'stronger' (costlier)
|
|
configuration without a real model — see runners/strong.py.
|
|
"""
|
|
name = "offline"
|
|
|
|
def __init__(self, *, cost_per_call: float = 0.002, tokens_per_call: int = 800,
|
|
time_per_call: float = 0.5):
|
|
self.cost_per_call = cost_per_call
|
|
self.tokens_per_call = tokens_per_call
|
|
self.time_per_call = time_per_call
|
|
|
|
def generate(self, *, objective: str, reference_files: Files, max_tokens: int) -> GenerationResult:
|
|
# Deterministic: same reference output regardless of system/topology (no swarm bias).
|
|
return GenerationResult(
|
|
files=list(reference_files),
|
|
tokens=self.tokens_per_call,
|
|
cost_usd=self.cost_per_call,
|
|
elapsed_s=self.time_per_call,
|
|
)
|
|
|
|
|
|
class OpenAIBackend(ExecutionBackend):
|
|
"""Real OpenAI-compatible generation. Lazy client; raises clearly if no key is configured."""
|
|
name = "openai"
|
|
|
|
def __init__(self, *, model: Optional[str] = None, price_per_1k_tokens: float = 0.0):
|
|
self.model = model or os.getenv("OPENAI_MODEL", "gpt-4o-mini")
|
|
self.price_per_1k_tokens = price_per_1k_tokens
|
|
self._client = None
|
|
|
|
def _get_client(self):
|
|
if self._client is None:
|
|
from openai import OpenAI # lazy: keep offline/CI import-light
|
|
if not os.getenv("OPENAI_API_KEY"):
|
|
raise RuntimeError("OpenAIBackend requires OPENAI_API_KEY (use OfflineBackend for hermetic runs)")
|
|
self._client = OpenAI(base_url=os.getenv("OPENAI_BASE_URL") or None)
|
|
return self._client
|
|
|
|
@staticmethod
|
|
def _parse_files(content: str) -> Files:
|
|
# Accept a JSON {"files":[{"path","content"}]} (same shape the agent uses), else empty.
|
|
try:
|
|
text = re.sub(r"^```(json)?|```$", "", content.strip(), flags=re.MULTILINE).strip()
|
|
data = json.loads(text)
|
|
return [(f["path"], f["content"]) for f in data.get("files", []) if f.get("path")]
|
|
except Exception:
|
|
return []
|
|
|
|
def generate(self, *, objective: str, reference_files: Files, max_tokens: int) -> GenerationResult:
|
|
import time as _time
|
|
client = self._get_client()
|
|
prompt = (
|
|
f"{objective}\n\nReturn ONLY JSON: "
|
|
'{"files":[{"path":"relative/path.py","content":"complete file content"}]}'
|
|
)
|
|
start = _time.time()
|
|
resp = client.chat.completions.create(
|
|
model=self.model,
|
|
messages=[{"role": "user", "content": prompt}],
|
|
max_tokens=max_tokens,
|
|
)
|
|
elapsed = _time.time() - start
|
|
content = resp.choices[0].message.content or ""
|
|
usage = getattr(resp, "usage", None)
|
|
tokens = int(getattr(usage, "total_tokens", 0) or 0)
|
|
cost = tokens / 1000.0 * self.price_per_1k_tokens
|
|
return GenerationResult(files=self._parse_files(content), tokens=tokens,
|
|
cost_usd=cost, elapsed_s=elapsed)
|