Files
Agentswarm/scripts/test-swarm-competition.py
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

109 lines
5.0 KiB
Python

"""Integration test for decentralized-rework P4: task competition (#8).
Agents bid for a task; a deterministic arbitrator picks the winner and assigns it. Agents can
yield a task back, and request takeover of a held task (only winning decisively reassigns it).
Hermetic, no model key.
Run from agent_swarm_v6 (install deps first — needs fakeredis):
pip install -r orchestrator/requirements.txt -r agent/requirements.txt
REDIS_FAKE=1 ENABLE_TASK_COMPETITION=1 python scripts/test-swarm-competition.py
"""
import asyncio
import os
import sys
from pathlib import Path
os.environ["REDIS_FAKE"] = "1"
os.environ["ENABLE_TASK_COMPETITION"] = "1"
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from orchestrator.redis_client import redis_client
from orchestrator import swarm_runtime as sr_mod
from orchestrator.swarm_runtime import swarm_runtime
from orchestrator.task_queue import task_queue, TaskStatus
from orchestrator.agent_registry import agent_registry
from orchestrator import main as orch
failures = []
def check(name, cond):
print(("PASS" if cond else "FAIL"), "-", name)
if not cond:
failures.append(name)
async def _noop(self, *a, **k):
return None
async def mk_task(run, tid, caps=None):
t = await task_queue.create_task(task_id=f"{run.swarm_id}-{tid}", description=tid,
agent_role="implementation",
required_capabilities=caps or ["python"], enqueue=True)
await swarm_runtime.attach_task(run, t.task_id)
return t
async def main():
await redis_client.connect()
sr_mod.SwarmRuntime._post_callback = _noop
body = {"mode": "swarm", "requirement": {"objective": "competition test"},
"callback": {"url": "http://127.0.0.1:9/cb", "subscribed_events": []},
"metadata": {"manager_deployment_id": "m-comp"}}
run, _ = await swarm_runtime.get_or_create_run(body=body, idempotency_key=None, correlation_id="cc")
for aid in ("bid-A", "bid-B", "yielder", "incumbent", "requester"):
await agent_registry.register_agent(aid, ["python"])
# --- 1. two agents bid → arbitrate → stronger wins + gets assigned ---
t1 = await mk_task(run, "t1")
await orch.handle_task_bid("bid-A", {"task_id": t1.task_id, "confidence": 0.9, "capabilities": ["python"]})
r = await orch.handle_task_bid("bid-B", {"task_id": t1.task_id, "confidence": 0.5, "capabilities": ["python"]})
check("two bids recorded", r.get("bid_count") == 2)
# arbitrate_and_assign is called by the loop with a freshly-fetched run (bids live in Redis).
run = await swarm_runtime.get_run(run.swarm_id)
arb = await orch.arbitrate_and_assign(run, t1.task_id)
check("arbitration picked higher-confidence bidder A", arb and arb["winner_agent_id"] == "bid-A")
check("arbitration decisive + losers recorded", arb and arb["decisive"] and arb["losers"] == ["bid-B"])
t1r = await task_queue.get_task(t1.task_id)
check("winner assigned the task", t1r.assigned_agent_id == "bid-A" and t1r.status != TaskStatus.PENDING)
run_r = await swarm_runtime.get_run(run.swarm_id)
check("bids cleared after arbitration", not (run_r.metadata.get("bids") or {}).get(t1.task_id))
check("arbitration audit recorded on run", bool(run_r.metadata.get("arbitrations")))
# --- 2. yield: an assigned task is released back to pending ---
t2 = await mk_task(run, "t2")
await task_queue.remove_pending_task(t2.task_id)
await task_queue.assign_task(t2.task_id, "yielder")
y = await orch.handle_task_yield("yielder", {"task_id": t2.task_id, "reason": "context too large", "recommend_agent": "bid-A"})
check("yield released the task", y.get("released") is True)
t2r = await task_queue.get_task(t2.task_id)
check("yielded task back to PENDING + agent cleared",
t2r.status == TaskStatus.PENDING and t2r.assigned_agent_id is None)
# --- 3. takeover: strong requester wins held task from incumbent ---
t3 = await mk_task(run, "t3")
await task_queue.remove_pending_task(t3.task_id)
await task_queue.assign_task(t3.task_id, "incumbent")
tk = await orch.handle_task_takeover("requester", {"task_id": t3.task_id, "confidence": 0.95, "capabilities": ["python"]})
check("decisive requester took over", tk.get("taken_over") is True and tk.get("winner_agent_id") == "requester")
t3r = await task_queue.get_task(t3.task_id)
check("task reassigned to requester", t3r.assigned_agent_id == "requester")
# --- 4. post-cutover: competition is unconditional (swarm default; no enable flag) ---
os.environ.pop("ENABLE_TASK_COMPETITION", None)
off = await orch.handle_task_bid("bid-A", {"task_id": t1.task_id, "confidence": 0.9, "capabilities": ["python"]})
check("bids processed without any flag (swarm default)", off.get("recorded") is True)
print()
if failures:
print(f"{len(failures)} competition (P4) check(s) FAILED: {failures}")
sys.exit(1)
print("all swarm competition (P4) checks passed")
if __name__ == "__main__":
asyncio.run(main())