feat(swarm): 协作聚合收敛取代蜂后选优 + sandbox 用 pytest 验证
CI / tests (push) Failing after 15m3s
CI / guardrails (push) Failing after 15m3s

按 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:
gongzhiyong
2026-06-20 22:03:00 +08:00
co-authored by Claude Opus 4.8
parent 28a6c618ab
commit 1288fd19d7
13 changed files with 1095 additions and 444 deletions
+25 -2
View File
@@ -403,6 +403,15 @@ class Agent:
except Exception as e:
logger.error(f"Failed to propose task: {e}")
@staticmethod
def _git_push_enabled() -> bool:
# New convergence architecture (feat/queen-convergence): worker agents no longer push a
# per-task git WORK branch into the delivery repo — intermediate artifacts ride in
# task.result["files"] and the orchestrator's aggregation node integrates them onto main.
# Default OFF. The legacy "commit + push agent/<id>/<task> branch" path runs ONLY when an
# operator explicitly opts in with AGENT_GIT_PUSH_ENABLED=true (e.g. for debugging).
return os.getenv("AGENT_GIT_PUSH_ENABLED", "false").lower() in {"1", "true", "yes"}
async def execute_assignment(self, assignment: TaskAssignment):
# Execute one accepted assignment, manage workspace/git flow, and publish lifecycle updates.
async with self.task_semaphore:
@@ -426,6 +435,7 @@ class Agent:
git_enabled = False
task_git = None
git_push_enabled = self._git_push_enabled()
try:
# Each task runs in its OWN git worktree cut from this agent's clone, so an agent can
# run several tasks concurrently without sharing a checkout/index. The worktree holds
@@ -433,7 +443,13 @@ class Agent:
# source there and the per-task GitOperations (task_git) commits/pushes that branch.
# This fixes the prior empty per-task subdir → "Empty workspace: no files detected".
# Falls back to the repo root (no isolated branch) only if worktree creation fails.
if await self.workspace_git.is_git_workspace():
#
# Convergence default (AGENT_GIT_PUSH_ENABLED unset/false): we do NOT cut a per-task
# result branch at all — there is nothing to commit/push because artifacts are
# returned in task.result["files"]. The task simply executes against the shared repo
# root for read context. The isolated worktree branch is created only in the legacy
# opt-in push path below.
if git_push_enabled and await self.workspace_git.is_git_workspace():
async with self._git_admin_lock: # serialize shared-repo git plumbing only
result_branch = await self.workspace_git.add_task_worktree(str(task_workspace), task_id)
if result_branch:
@@ -468,7 +484,14 @@ class Agent:
if result.get("success"):
self.last_summary = self._summarize_result(result) or self.last_summary
if result.get("success") and not awaiting_handoff:
if git_enabled and task_git:
if not git_push_enabled:
# Convergence default: artifacts are carried in result["files"]; the
# orchestrator aggregation node integrates them. No work branch is pushed.
result["git_skipped"] = (
"git push disabled (AGENT_GIT_PUSH_ENABLED=false); "
"artifacts returned in task.result.files"
)
elif git_enabled and task_git:
commit_sha = await task_git.commit_changes(
message=f"Task {task_id}: {description[:50]}"
)
+39
View File
@@ -143,6 +143,14 @@ class TaskExecutor:
"agent_id": self.agent_id,
"usage": self._usage_payload(time.time() - started_at),
}
# Hoist every subtask's generated files to a top-level `files` array so the produced
# artifacts travel in task.result and reach the orchestrator's aggregation node WITHOUT
# a git push of a work branch (the swarm's new convergence path; orchestrator
# quality.collect_generated_files consumes result['files']). Each entry keeps the frozen
# shape {path, content, action} with the COMPLETE file content; last writer wins per path
# so a later subtask that rewrites a file supersedes an earlier one. Deletes are carried
# through as {path, action:"delete"} (no content) for the aggregator to honor.
payload["files"] = self._aggregate_subtask_files(results)
if not success:
# Surface the model's OWN failure explanation (what it saw / what was missing) as a
# top-level `error`, so the orchestrator/Manager records WHY instead of a generic
@@ -175,6 +183,37 @@ class TaskExecutor:
finally:
self.current_context = {}
@staticmethod
def _aggregate_subtask_files(results: list[dict]) -> list[dict]:
"""Flatten the per-subtask file specs into one ordered, de-duplicated `files` list.
Each subtask result carries the model's `files` (the same {path, action, content} specs the
executor applied to the workspace). We re-emit them at the top level so the artifacts travel
in task.result instead of a pushed git branch. Last writer wins per path (a later subtask
rewriting/deleting a file supersedes an earlier write); ordering follows last occurrence.
Only writes with non-None content and deletes are kept; malformed entries are skipped.
"""
by_path: dict[str, dict] = {}
for r in results:
if not isinstance(r, dict):
continue
for f in (r.get("files") or []):
if not isinstance(f, dict):
continue
path = f.get("path")
if not path:
continue
action = f.get("action", "write")
if action == "delete":
by_path[path] = {"path": path, "action": "delete"}
elif action == "write":
content = f.get("content")
if content is None:
continue # an incomplete write (no content) is not a usable artifact
by_path[path] = {"path": path, "content": content, "action": "write"}
# unknown actions are ignored (the orchestrator only consumes write/delete)
return list(by_path.values())
async def _maybe_propose_subtasks(self, task_id: str, description: str, context: dict,
proposal_callback: Optional[Callable]) -> int:
"""agent_swarm#7: decompose a top-level seed and propose each subtask to the shared pool.