feat(swarm): 协作聚合收敛取代蜂后选优 + sandbox 用 pytest 验证
按 juejin 协作聚合模型重构收敛(取代 best-of-N 选优): - 删蜂后选优(queen.py/test-queen.py) - 新增聚合节点 result_aggregator.py:共享池收集→同文件 LLM/AST 整合→沙箱验证→单次落 main - 质量驱动闭环:不达标打回迭代(AGGREGATE_ACCEPTANCE_THRESHOLD + MAX_REVIEW_CYCLES) - agent 停 git 工作分支,产出走 task.result.files 共享池(AGENT_GIT_PUSH_ENABLED 默认 false) - sandbox_runner 改用 pytest(原生支持 pytest 风格 class),修 stdlib runner 收集失败 - 文档同步重写为协作聚合模型 本地验证:产物仓单分支 main + 三函数完整 + pytest 12/12 pass_rate=100 一次达标。 影响:Swarm 收敛/聚合层;Manager/客户端契约不变(artifact字段/sequence/状态机;契约测试全过)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
28a6c618ab
commit
1288fd19d7
+40
-65
@@ -37,7 +37,7 @@ from . import autonomous_tasks as autonomous_mod
|
||||
from . import task_competition as competition_mod
|
||||
from . import cross_review as cross_review_mod
|
||||
from . import guard as guard_mod
|
||||
from . import queen as queen_mod
|
||||
from . import result_aggregator
|
||||
from . import agent_launcher
|
||||
|
||||
# Configure logging
|
||||
@@ -698,12 +698,29 @@ async def refresh_swarm_run_status(run):
|
||||
if next_status == "completed":
|
||||
if await run_cross_review(run, tasks):
|
||||
return
|
||||
# Queen quality gate (M3/SC-9): score the candidates and, if the best fails the acceptance
|
||||
# bar (and the review-cycle cap isn't hit), send work BACK for another round instead of
|
||||
# declaring success on substandard output. Disabled by default (no threshold) — keeps
|
||||
# current completion semantics until an operator sets QUEEN_ACCEPTANCE_THRESHOLD.
|
||||
if await queen_quality_gate(run, tasks):
|
||||
# 协作聚合质量门(①合并 ②沙箱验证 ③不达标打回):聚合节点合并各 agent 互补产出 → 跑测试验证 →
|
||||
# 不达标且未到轮次上限则 reopen 打回(run 保持 running,下轮重做),达标/达上限则落 main。
|
||||
# 阈值未设(AGGREGATE_ACCEPTANCE_THRESHOLD)时不打回,保持原完成语义。
|
||||
agg_cycles = int(run.metadata.get("review_cycles", 0) or 0)
|
||||
agg_max = int(os.getenv("MAX_REVIEW_CYCLES", "2") or 2)
|
||||
agg = await result_aggregator.finalize_run(
|
||||
run, tasks, run.objective, _accept_threshold(run), agg_cycles, agg_max)
|
||||
run.metadata["aggregation"] = agg
|
||||
if agg.get("decision") == "bounce":
|
||||
from .quality import collect_generated_files
|
||||
reopened = 0
|
||||
for t in tasks:
|
||||
if collect_generated_files([t]).get("impl"):
|
||||
if await task_queue.reopen_task(t.task_id):
|
||||
reopened += 1
|
||||
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)
|
||||
logger.info("aggregation: bounced run %s — reopened %d for rework (cycle %d, pass_rate=%s)",
|
||||
run.swarm_id, reopened, agg_cycles + 1,
|
||||
(agg.get("validation") or {}).get("pass_rate"))
|
||||
return
|
||||
await swarm_runtime.save_run(run)
|
||||
|
||||
if run.status == next_status:
|
||||
return
|
||||
@@ -734,27 +751,15 @@ async def refresh_swarm_run_status(run):
|
||||
results = {t.task_id: {"result": parse_task_result(t) or {}} for t in completed}
|
||||
final_summary = await master_agent.synthesize(run.objective, results)
|
||||
run.metadata["final_summary"] = final_summary
|
||||
# Queen (agent_swarm#8/#12): aggregate the fan-out agents' artifacts, score each by running
|
||||
# its impl against the swarm's shared tests, and SELECT the single best (best-of-N). Records
|
||||
# the verdict on the run and marks the winner on the deliverable so the result is one
|
||||
# coherent pick, not N scattered branches. Best-effort: never breaks the terminal path.
|
||||
# The Queen verdict was computed by the quality gate above (run.metadata['queen']); mark the
|
||||
# selected winner on the deliverable so the result is one coherent pick, not N branches.
|
||||
queen_summary = run.metadata.get("queen") or {}
|
||||
winner = queen_summary.get("winner") if isinstance(queen_summary, dict) else None
|
||||
if isinstance(deliverable, dict) and winner:
|
||||
deliverable["selected"] = winner
|
||||
deliverable["candidate_count"] = queen_summary.get("candidate_count")
|
||||
# SC-7: promote the winning artifact to the repo's main → one coherent deliverable on
|
||||
# main, not N scattered agent branches. No-op when the run has no git grant.
|
||||
try:
|
||||
promo = await queen_mod.promote_to_main(run, tasks, winner.get("task_id"))
|
||||
run.metadata["queen_promotion"] = promo
|
||||
if isinstance(deliverable, dict) and promo.get("promoted"):
|
||||
deliverable["promoted_to_main"] = {
|
||||
"branch": promo.get("branch"), "commit_sha": promo.get("commit_sha")}
|
||||
except Exception as exc:
|
||||
logger.warning("queen promote_to_main failed for run %s: %s", run.swarm_id, exc)
|
||||
# 聚合产物已由上面的协作聚合质量门产出并落 main(run.metadata['aggregation']);折进 deliverable。
|
||||
agg = run.metadata.get("aggregation") or {}
|
||||
if isinstance(deliverable, dict) and agg.get("finalized"):
|
||||
deliverable["aggregated_files"] = agg.get("files")
|
||||
deliverable["merged_file_count"] = agg.get("merged_file_count")
|
||||
promo = agg.get("promotion") or {}
|
||||
if promo.get("promoted"):
|
||||
deliverable["promoted_to_main"] = {
|
||||
"branch": promo.get("branch"), "commit_sha": promo.get("commit_sha")}
|
||||
await swarm_runtime.save_run(run)
|
||||
|
||||
# Benchmark Group B: grade the run's generated code against its held-out fixture tests in
|
||||
@@ -1175,48 +1180,18 @@ async def run_cross_review(run, tasks) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def _queen_threshold(run) -> Optional[float]:
|
||||
"""Queen acceptance bar (pass_rate 0-100): run.metadata override → QUEEN_ACCEPTANCE_THRESHOLD
|
||||
env → None (gate disabled). None keeps the current task-completion completion semantics."""
|
||||
raw = (run.metadata or {}).get("queen_acceptance_threshold")
|
||||
def _accept_threshold(run) -> Optional[float]:
|
||||
"""协作聚合验收阈值(pass_rate 0-100):run.metadata 覆盖 → AGGREGATE_ACCEPTANCE_THRESHOLD env →
|
||||
None(门禁用)。None 保持当前完成语义(不打回);设了才启用质量驱动的"不达标打回迭代"。"""
|
||||
raw = (run.metadata or {}).get("aggregate_acceptance_threshold")
|
||||
if raw is None:
|
||||
raw = os.getenv("QUEEN_ACCEPTANCE_THRESHOLD")
|
||||
raw = os.getenv("AGGREGATE_ACCEPTANCE_THRESHOLD")
|
||||
try:
|
||||
return float(raw) if raw not in (None, "") else None
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
async def queen_quality_gate(run, tasks) -> bool:
|
||||
"""M3/SC-9: the Queen scores the fan-out candidates and, if the best fails the acceptance bar
|
||||
and the review-cycle cap isn't hit, sends work BACK (reopen impl tasks) for another round
|
||||
instead of declaring success on substandard output. Stores the verdict on run.metadata['queen']
|
||||
(reused by the deliverable). Returns True if it reopened (caller keeps the run RUNNING).
|
||||
Best-effort: never raises, never bounces on a score it couldn't compute (org rule #9)."""
|
||||
try:
|
||||
summary = await queen_mod.aggregate_run(run, tasks)
|
||||
run.metadata["queen"] = summary
|
||||
cycles = int(run.metadata.get("review_cycles", 0) or 0)
|
||||
max_cycles = int(os.getenv("MAX_REVIEW_CYCLES", "2") or 2)
|
||||
if not queen_mod.should_bounce(summary, _queen_threshold(run), cycles, max_cycles):
|
||||
await swarm_runtime.save_run(run)
|
||||
return False
|
||||
reopened = 0
|
||||
for c in summary.get("candidates", []):
|
||||
if await task_queue.reopen_task(c["task_id"]):
|
||||
reopened += 1
|
||||
run.metadata["review_cycles"] = cycles + 1
|
||||
# SC-10: tally quality-driven reopens for P_rework (reopen_task doesn't bump retry_count).
|
||||
run.metadata["rework_reopens"] = int(run.metadata.get("rework_reopens", 0) or 0) + reopened
|
||||
await swarm_runtime.save_run(run)
|
||||
logger.info("queen: quality gate bounced run %s — reopened %d for rework (cycle %d)",
|
||||
run.swarm_id, reopened, cycles + 1)
|
||||
return reopened > 0
|
||||
except Exception as exc:
|
||||
logger.warning("queen quality gate failed for run %s: %s", run.swarm_id, exc)
|
||||
return False
|
||||
|
||||
|
||||
async def _historical_success_map(agent_role: str, agent_ids) -> Dict[str, float]:
|
||||
"""τ (decision_engine pheromone) per agent, normalized to [0,1] for arbitration."""
|
||||
out: Dict[str, float] = {}
|
||||
@@ -1237,7 +1212,7 @@ async def handle_task_bid(agent_id: str, message: Dict[str, Any]) -> Dict[str, A
|
||||
if not run or not task_id:
|
||||
return {"recorded": False, "reason": "no_run"}
|
||||
if not _agent_belongs_to_run(agent_id, run):
|
||||
logger.warning("queen: rejected cross-run bid — agent %s ∉ run %s (task %s)",
|
||||
logger.warning("run-boundary: rejected cross-run bid — agent %s ∉ run %s (task %s)",
|
||||
agent_id, run.swarm_id, task_id)
|
||||
return {"recorded": False, "reason": "cross_run_denied"}
|
||||
bid = competition_mod.TaskBid(
|
||||
@@ -1296,7 +1271,7 @@ async def handle_task_yield(agent_id: str, message: Dict[str, Any]) -> Dict[str,
|
||||
if not run or not task_id:
|
||||
return {"released": False, "reason": "no_run"}
|
||||
if not _agent_belongs_to_run(agent_id, run):
|
||||
logger.warning("queen: rejected cross-run yield — agent %s ∉ run %s (task %s)",
|
||||
logger.warning("run-boundary: rejected cross-run yield — agent %s ∉ run %s (task %s)",
|
||||
agent_id, run.swarm_id, task_id)
|
||||
return {"released": False, "reason": "cross_run_denied"}
|
||||
yield_msg = competition_mod.TaskYield(
|
||||
@@ -1325,7 +1300,7 @@ async def handle_task_takeover(agent_id: str, message: Dict[str, Any]) -> Dict[s
|
||||
if not task or not run:
|
||||
return {"taken_over": False, "reason": "no_task"}
|
||||
if not _agent_belongs_to_run(agent_id, run):
|
||||
logger.warning("queen: rejected cross-run takeover — agent %s ∉ run %s (task %s)",
|
||||
logger.warning("run-boundary: rejected cross-run takeover — agent %s ∉ run %s (task %s)",
|
||||
agent_id, run.swarm_id, task_id)
|
||||
return {"taken_over": False, "reason": "cross_run_denied"}
|
||||
incumbent_id = task.assigned_agent_id
|
||||
|
||||
Reference in New Issue
Block a user