把本仓从「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>
131 lines
5.5 KiB
Python
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())
|