四块互相交织的 benchmark 覆盖增量,统一提交: 1) 通信遥测(#23):orchestrator 路由 peer 消息时按 correlation_id 计请求/应答到 SwarmRun.collaboration(内部状态,不进 Manager 事件流);collector 算 s_communication。 治理计数由 run.approvals 派生(合规/总数)→ s_governance。 2) Q_quality 掩码归一(v2.1 裁定):metrics.quality_score 改为对 present 输入加权归一, 非编码任务自动忽略 TestPassRate,全缺 → NaN(不伪造)。 3) 质量插桩 / Group B:新增 Pod 内代码测试沙箱(orchestrator/sandbox.py,环境清洗 + 超时强杀 + 资源限额 + 路径越界校验,门控 ENABLE_QUALITY_EVAL)与 held-out fixture (benchmark/fixtures/);run 完成时用留出测试评分得 TestPassRate → Q_quality → collector 合成 reward。安全边界见 docs/integration/security-boundary.md §8.1。 4) 决策引擎 / Group A(#10,Option A score-at-pull):新增 orchestrator/decision_engine.py —— 信息素 τ(Redis 持久、(role,agent) 键控、冷启动 0.5、ρ 蒸发、夹紧、学习常开)+ η 启发式评分 + ε-greedy 概率采样;每次 dispatch 产一条 DecisionTrace → SwarmRun.decisions;collector 算 tau/eta/p_decision。概率选择门控 ENABLE_ACO_DISPATCH (默认关,CI 用 ACO_SEED 固定)。 覆盖:单次 run 真实可算字段由 4 提升至最多 10/15(新增 communication/reward/tau/eta/ p_decision,外加 governance 有条件)。 测试:新增 test-sandbox / test-quality / test-decision-engine;扩充 collector/metrics 用例; CI 纳入全部 benchmark 套件 + flag-on 的 ACO e2e。本地 11 项 gate 全绿。 诚实边界(未越界声称): - Group A 为单边匹配(Option B 待 Group C);概率派发优于贪心未证;默认关闭。 - reward 的 CodeReview/UserAcceptance 未采集(掩码忽略);P_risk 为审批派生低估。 - s_gain/s_swarm/g_e/g_e_cost/benchmark 仍 NaN —— 需基线(#21/#13),本 PR 不动验收。 影响范围:Swarm(orchestrator + benchmark + docs + CI)。不改 Manager↔Swarm 事件契约 (遥测均为运行时内部状态);不影响 Client/计费/密钥/发布链路。新增 ENABLE_QUALITY_EVAL / ENABLE_ACO_DISPATCH 两个开关,默认关闭。 Closes #10 Closes #23 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
122 lines
5.5 KiB
Python
122 lines
5.5 KiB
Python
"""Test SwarmRunMetricsCollector against a synthetic run with known states.
|
|
|
|
Hermetic (REDIS_FAKE). Run from agent_swarm_v6: python scripts/test-benchmark-collector.py
|
|
"""
|
|
import asyncio
|
|
import json
|
|
import math
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
os.environ["REDIS_FAKE"] = "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 benchmark.collectors.run_collector import SwarmRunMetricsCollector
|
|
|
|
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 add_task(run, task_id, *, status, agent, cost=0.0, retry=0, depends_on=None):
|
|
t = await task_queue.create_task(task_id=task_id, description=task_id, agent_role=task_id.split("-")[-1],
|
|
depends_on=depends_on or [], enqueue=False)
|
|
t.status = status
|
|
t.assigned_agent_id = agent
|
|
t.retry_count = retry
|
|
t.result = json.dumps({"usage": {"model_cost_usd": cost}})
|
|
await task_queue._save_task(t)
|
|
await swarm_runtime.attach_task(run, t.task_id)
|
|
return t
|
|
|
|
|
|
async def main():
|
|
await redis_client.connect()
|
|
sr_mod.SwarmRuntime._post_callback = _noop # no real HTTP
|
|
|
|
body = {
|
|
"mode": "swarm",
|
|
"requirement": {"objective": "collector test"},
|
|
"orchestration_plan": {"budget": {"max_cost_usd": 10}},
|
|
"callback": {"url": "http://127.0.0.1:9/cb", "subscribed_events": []},
|
|
"metadata": {"manager_deployment_id": "m-col"},
|
|
}
|
|
run, _ = await swarm_runtime.get_or_create_run(body=body, idempotency_key=None, correlation_id="c")
|
|
|
|
await add_task(run, "t-implementation", status=TaskStatus.COMPLETED, agent="A", cost=2.0)
|
|
await add_task(run, "t-testing", status=TaskStatus.COMPLETED, agent="B", cost=3.0, retry=1,
|
|
depends_on=["t-implementation"])
|
|
await add_task(run, "t-documentation", status=TaskStatus.FAILED, agent="A", cost=0.0, retry=3)
|
|
|
|
await swarm_runtime.emit_event(run, "handoff.requested", payload={"task_id": "t1", "from_role": "implementation", "to_role": "testing"})
|
|
await swarm_runtime.emit_event(run, "handoff.completed", payload={"task_id": "t1", "from_role": "implementation", "to_role": "testing"})
|
|
|
|
# Peer communication: 3 requests routed, 2 of them answered (matching correlation_ids).
|
|
for cid, delivered in [("c1", True), ("c2", True), ("c3", True)]:
|
|
await swarm_runtime.record_peer_message(run, correlation_id=cid, is_reply=False, delivered=delivered)
|
|
for cid in ("c1", "c2"): # c3 never gets a reply
|
|
await swarm_runtime.record_peer_message(run, correlation_id=cid, is_reply=True, delivered=True)
|
|
|
|
collector = SwarmRunMetricsCollector(run.swarm_id)
|
|
metrics = await collector.collect()
|
|
cov = collector.coverage
|
|
|
|
# s_completion = 2/3*100
|
|
check("s_completion = 66.67", round(metrics.s_completion, 2) == 66.67 and cov["s_completion"])
|
|
# s_collaboration = 0.5*100(handoff) + 0.3*100(dep resolved) + 0.2*50(workload A:2,B:1) = 90
|
|
check("s_collaboration = 90.0", round(metrics.s_collaboration, 1) == 90.0 and cov["s_collaboration"])
|
|
# s_robustness: failures={t-testing(retry),t-documentation(failed)}=2, recovered={t-testing completed}=1 => 50
|
|
check("s_robustness = 50.0", round(metrics.s_robustness, 1) == 50.0 and cov["s_robustness"])
|
|
# s_cost = 100*budget(10)/actual(5) = 200
|
|
check("s_cost = 200.0", round(metrics.s_cost, 1) == 200.0 and cov["s_cost"])
|
|
# governance: no approvals -> NaN, coverage False
|
|
check("s_governance NaN + coverage False", math.isnan(metrics.s_governance) and cov["s_governance"] is False)
|
|
# s_communication = 2 answered / 3 requests * 100 = 66.67
|
|
check("s_communication = 66.67", round(metrics.s_communication, 2) == 66.67 and cov["s_communication"])
|
|
# not-yet-collectable -> NaN + coverage False
|
|
check("uncollectable metrics NaN + coverage False",
|
|
all(math.isnan(getattr(metrics, k)) and cov[k] is False
|
|
for k in ("tau", "eta", "p_decision", "reward", "s_gain",
|
|
"s_swarm", "g_e", "g_e_cost", "benchmark")))
|
|
|
|
# --- governance telemetry counter: REAL when the run had governed ops (#23) ---
|
|
# CompliantOperations/TotalOperations: 2 approvals decided (approved+rejected) of 3 governed
|
|
# ops -> 66.67. Proves the governance counter is wired, not just the empty/NaN path above.
|
|
gov_run, _ = await swarm_runtime.get_or_create_run(
|
|
body={**body, "metadata": {"manager_deployment_id": "m-gov"}},
|
|
idempotency_key=None, correlation_id="cg")
|
|
gov_run.approvals = {
|
|
"a1": {"approval_id": "a1", "decision": "approved"},
|
|
"a2": {"approval_id": "a2", "decision": "rejected"},
|
|
"a3": {"approval_id": "a3", "decision": "pending"},
|
|
}
|
|
await add_task(gov_run, "g-implementation", status=TaskStatus.COMPLETED, agent="A", cost=1.0)
|
|
await swarm_runtime.save_run(gov_run)
|
|
gov_collector = SwarmRunMetricsCollector(gov_run.swarm_id)
|
|
gm = await gov_collector.collect()
|
|
check("s_governance = 66.67 (2 decided / 3 governed ops)",
|
|
round(gm.s_governance, 2) == 66.67 and gov_collector.coverage["s_governance"] is True)
|
|
|
|
print()
|
|
if failures:
|
|
print(f"{len(failures)} collector check(s) FAILED: {failures}")
|
|
sys.exit(1)
|
|
print("all benchmark collector checks passed")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|