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
+25
-2
@@ -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]}"
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user