Files
Songhaoz666andClaude Opus 4.8 f281370209 去中心化蜂群重构:从 Master 中心化切换为播种+自组织(唯一行为)
把本仓从「Master 分解→派发→单评审」重构为去中心化自组织蜂群,并设为唯一行为
(本仓即 swarm 运行时,模式选择在仓外,无 ENABLE_swarm 开关)。依据:heicodeDocs 弱中心
定义 + 蜂群文章(stigmergy)+ OpenAI Swarm(handoff)。

流程(全部无条件生效):
- 播种 build_seed_task_specs(不主控分解)→ 自选 swarm_dispatch(能力+τ+负载+预算,唯一派发)
  → 自主分解 task_proposal(autonomous_tasks)→ 竞争/接管 task_bid/yield/takeover(task_competition)
  → 同伴交叉评审 ≥2 评审者(cross_review,取代单 critic)→ 收敛 ConvergenceReport+termination_reason
  (convergence)→ 守卫 guard.diagnose(检测无法运作并列原因)。

删除:planner 主控分解、贪心/ACO/scored 派发与 scored_matchmake、单 critic 评审环、
全部 ENABLE_* swarm 构建开关。master_agent.synthesize 作为汇总工具保留。

测试:test-workflow-e2e 改写为真实 swarm 全流程;test-merge-smoke 改为 seeder/cross-review;
新增 test-swarm-{seed,dispatch,autonomous,competition,cross-review,convergence,guard} + 模块单测;
CI 同步。本地 18 套全绿。

影响范围:agent_swarm(orchestrator + 新模块 + 测试 + docs + CI)。不改 Manager↔Swarm 事件契约
(新事件均为运行时内部状态/遥测,不进 HM 注册表);不影响 Client/计费/密钥/审计/发布链路。
无新增长期开关。

诚实边界:不动 benchmark 验收(G_E 仍需真实 run,#13);convergence 暂为解释性(不强制覆盖
run.status);前端可见状态为跨端(#18);「去中心化是否更优」未证(需 Group C)。

#6 的 DoD(Path B 决策 + 定位文档)本 PR 已满足并实现 Path B → 关闭 #6。
#7/#8/#11/#12 的运行时机制已落地(事件 + e2e),但其 DoD 含前端可见状态(跨端 #18)与
多 Agent 回放,故以 Refs 链接、不自动关闭,留维护者在前端/验收就绪后关闭。

Closes #6
Refs #7
Refs #8
Refs #11
Refs #12
Refs #18

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 16:11:47 +08:00

131 lines
5.5 KiB
Python

"""Keyless stub agent for the SWARM workflow test (decentralized flow).
Connects over WebSocket, registers, and drives the swarm flow WITHOUT any LLM (no key needed):
when it self-selects the SEED task it PROPOSES the subtasks (bottom-up decomposition, #7) and then
completes the seed; for every subsequently self-selected subtask it returns a canned result. This
exercises seed → agent-proposed decomposition → self-selected execution → convergence — with no
Master plan and no central dispatch.
Run standalone against a manually started orchestrator:
set "ORCHESTRATOR_URL=ws://localhost:8000"
set "AGENT_ID=stub-1"
set "AGENT_CAPABILITIES=python,code_generation,testing,pytest,technical-writing,general"
python scripts/stub_agent.py
"""
import asyncio
import json
import os
import time
import websockets
# Subtasks the agent proposes when it perceives the seed (bottom-up decomposition).
SEED_SUBTASKS = [
{"description": "Implement the core add(a, b) function in calc.py",
"agent_role": "implementation", "required_capabilities": ["python", "code_generation"]},
{"description": "Write pytest unit tests covering add(a, b)",
"agent_role": "testing", "required_capabilities": ["testing", "pytest"]},
{"description": "Document usage of add(a, b) in the README",
"agent_role": "documentation", "required_capabilities": ["technical-writing", "general"]},
]
class StubAgent:
def __init__(self, orchestrator_url: str, agent_id: str, capabilities: list[str]):
self.orchestrator_url = orchestrator_url.rstrip("/")
self.agent_id = agent_id
self.capabilities = capabilities
self.ws = None
self.proposed = False # only decompose the seed once
self.running = True
def _result_for(self, task_id: str, description: str) -> dict:
return {
"success": True,
"task_id": task_id,
"subtasks": [{"status": "completed", "summary": f"completed: {description[:60]}",
"changes": description[:60], "files": []}],
"awaiting_handoff": False,
"agent_id": self.agent_id,
"usage": {
"model_id": "stub", "model_tokens": 0, "prompt_tokens": 0,
"completion_tokens": 0, "model_cost_usd": 0.0, "runtime_seconds": 0.0,
},
}
async def send(self, payload: dict):
await self.ws.send(json.dumps(payload))
async def handle(self, msg: dict):
msg_type = msg.get("type")
if msg_type == "task_assignment":
task_id = msg["task_id"]
context = msg.get("context") or {}
await self.send({"type": "task_accepted", "task_id": task_id, "available_slots": 4})
await self.send({"type": "task_start", "agent_id": self.agent_id, "task_id": task_id, "timestamp": time.time()})
await asyncio.sleep(0.2) # simulate work
# Perceived the seed → propose the subtasks (decentralized decomposition), then finish it.
if context.get("is_seed") and not self.proposed:
self.proposed = True
for sub in SEED_SUBTASKS:
await self.send({
"type": "task_proposal", "agent_id": self.agent_id,
"origin_task_id": task_id, "confidence": 0.9,
"reason": "decomposing the seed objective into specialist subtasks",
"trigger_event": "task.assignment", **sub,
})
await asyncio.sleep(0.4) # let the orchestrator review + enqueue the proposals
await self.send({
"type": "task_complete", "agent_id": self.agent_id,
"task_id": task_id, "result": self._result_for(task_id, msg.get("description", "")),
"timestamp": time.time(),
})
elif msg_type == "peer_message" and not msg.get("is_reply"):
await self.send({
"type": "peer_message", "agent_id": self.agent_id,
"target_agent_id": msg.get("from_agent_id"), "task_id": msg.get("task_id"),
"content": f"stub guidance from {self.agent_id}",
"correlation_id": msg.get("correlation_id"), "is_reply": True, "timestamp": time.time(),
})
async def heartbeat(self):
while self.running:
try:
await self.send({
"type": "heartbeat", "agent_id": self.agent_id,
"timestamp": time.time(), "active_tasks": 0, "available_slots": 4,
})
except Exception:
return
await asyncio.sleep(5)
async def run(self):
async with websockets.connect(f"{self.orchestrator_url}/ws/{self.agent_id}") as ws:
self.ws = ws
await self.send({
"type": "register", "agent_id": self.agent_id,
"capabilities": self.capabilities, "available_slots": 4, "active_task_ids": [],
})
hb = asyncio.create_task(self.heartbeat())
try:
async for raw in ws:
await self.handle(json.loads(raw))
finally:
self.running = False
hb.cancel()
async def _main():
agent = StubAgent(
os.getenv("ORCHESTRATOR_URL", "ws://localhost:8000"),
os.getenv("AGENT_ID", "stub-1"),
os.getenv("AGENT_CAPABILITIES", "python,code_generation,testing,pytest,technical-writing,general").split(","),
)
await agent.run()
if __name__ == "__main__":
asyncio.run(_main())