去中心化蜂群重构:从 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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
62610a7e3f
commit
f281370209
@@ -0,0 +1,114 @@
|
||||
"""Integration test for decentralized-rework P3: agent-driven decomposition (#7).
|
||||
|
||||
An executing agent proposes follow-up tasks from the shared seed/run state; the orchestrator
|
||||
reviews each (confidence floor / dedup-merge / per-run budget) and enqueues accepted ones as real
|
||||
PENDING tasks with full lineage. This is the bottom-up decomposition that replaces the Master plan.
|
||||
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_AGENT_TASK_PROPOSALS=1 AGENT_PROPOSAL_BUDGET=3 python scripts/test-swarm-autonomous.py
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
os.environ["REDIS_FAKE"] = "1"
|
||||
os.environ["ENABLE_AGENT_TASK_PROPOSALS"] = "1"
|
||||
os.environ["AGENT_PROPOSAL_BUDGET"] = "3"
|
||||
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
|
||||
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 propose(seed_id, desc, *, agent="agent-A", conf=0.9, role="general", caps=None):
|
||||
return await orch.handle_task_proposal(agent, {
|
||||
"origin_task_id": seed_id, "description": desc, "reason": "discovered gap in shared state",
|
||||
"confidence": conf, "agent_role": role, "required_capabilities": caps or [],
|
||||
"trigger_event": "task.completed",
|
||||
})
|
||||
|
||||
|
||||
async def main():
|
||||
await redis_client.connect()
|
||||
sr_mod.SwarmRuntime._post_callback = _noop
|
||||
|
||||
body = {"mode": "swarm", "requirement": {"objective": "Build a CSV parser"},
|
||||
"callback": {"url": "http://127.0.0.1:9/cb", "subscribed_events": []},
|
||||
"metadata": {"manager_deployment_id": "m-auto"}}
|
||||
run, _ = await swarm_runtime.get_or_create_run(body=body, idempotency_key=None, correlation_id="ca")
|
||||
seed = await task_queue.create_task(task_id=f"{run.swarm_id}-seed", description="Build a CSV parser",
|
||||
agent_role="general", required_capabilities=[], enqueue=False)
|
||||
await swarm_runtime.attach_task(run, seed.task_id)
|
||||
|
||||
# 1) two distinct high-confidence proposals → ACCEPT → real tasks with lineage
|
||||
r1 = await propose(seed.task_id, "Write unit tests for the CSV parser module", role="testing", caps=["testing"])
|
||||
r2 = await propose(seed.task_id, "Write API documentation for the parser", role="documentation")
|
||||
check("proposal 1 accepted → task created", r1.get("accepted") and r1.get("task_id"))
|
||||
check("proposal 2 accepted → task created", r2.get("accepted") and r2.get("task_id"))
|
||||
|
||||
t1 = await task_queue.get_task(r1["task_id"])
|
||||
check("created task is agent_proposed (bottom-up, not Master)", t1 and t1.source == "agent_proposed")
|
||||
lineage = (t1.context or {}).get("proposal") or {}
|
||||
check("created task carries lineage (proposer + origin + reason)",
|
||||
lineage.get("proposed_by_agent_id") == "agent-A"
|
||||
and lineage.get("origin_task_id") == seed.task_id
|
||||
and lineage.get("proposal_reason"))
|
||||
|
||||
# 2) near-duplicate of proposal 1 → MERGE (not a new task)
|
||||
rdup = await propose(seed.task_id, "Write unit tests for the CSV parser module")
|
||||
check("duplicate proposal → MERGE", rdup.get("decision") == "merge" and not rdup.get("accepted"))
|
||||
check("merge names the target task", rdup.get("merge_target_task_id") == r1["task_id"])
|
||||
|
||||
# 3) third distinct proposal → ACCEPT (budget 3: A,B + this = 3)
|
||||
r3 = await propose(seed.task_id, "Add a benchmark harness measuring parser throughput", role="general")
|
||||
check("proposal 3 accepted (within budget)", r3.get("accepted"))
|
||||
|
||||
# 4) fourth distinct proposal → budget exhausted → REJECT
|
||||
r4 = await propose(seed.task_id, "Containerize the parser service with a Dockerfile")
|
||||
check("proposal 4 rejected (budget exhausted)",
|
||||
not r4.get("accepted") and r4.get("decision") == "reject" and "budget" in (r4.get("reason") or ""))
|
||||
|
||||
# 5) low-confidence proposal → REJECT
|
||||
r5 = await propose(seed.task_id, "Maybe refactor something unspecified", conf=0.2)
|
||||
check("low-confidence proposal rejected",
|
||||
not r5.get("accepted") and r5.get("decision") == "reject")
|
||||
|
||||
# run grew from 1 seed → 1 seed + 3 accepted = 4 tasks; proposals telemetry recorded
|
||||
refreshed = await swarm_runtime.get_run(run.swarm_id)
|
||||
check("run task graph grew bottom-up to 4 tasks (seed + 3)", len(refreshed.task_ids) == 4)
|
||||
check("proposal lifecycle telemetry recorded on run",
|
||||
bool(refreshed.metadata.get("proposals")))
|
||||
check("accepted-proposal counter = 3", refreshed.metadata.get("proposal_accepted_count") == 3)
|
||||
|
||||
# Post-cutover: proposals are unconditional (swarm is the runtime; no enable flag).
|
||||
os.environ.pop("ENABLE_AGENT_TASK_PROPOSALS", None)
|
||||
roff = await propose(seed.task_id, "Add a CLI entrypoint for the parser")
|
||||
check("proposals processed without any flag (swarm default)", roff.get("decision") != "disabled")
|
||||
|
||||
print()
|
||||
if failures:
|
||||
print(f"{len(failures)} autonomous-task (P3) check(s) FAILED: {failures}")
|
||||
sys.exit(1)
|
||||
print("all swarm autonomous-task (P3) checks passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user