diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..63e941c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + +jobs: + guardrails: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # Defense in depth: fail if secrets or heavy/generated dirs were ever committed. + - name: Block secrets & node_modules + run: | + if git ls-files | grep -E '(^|/)\.env($|\.)|(^|/)secrets/|\.pem$|\.key$|\.p12$|\.pfx$|(^|/)id_rsa$|(^|/)id_ed25519$'; then + echo "::error::Secret-like files are tracked — remove them and rotate any exposed credential."; exit 1 + fi + if git ls-files | grep -E '(^|/)node_modules/'; then + echo "::error::node_modules is tracked — it must be gitignored."; exit 1 + fi + + - name: Required standards files present + run: | + for f in CLAUDE.md PROJECT_STANDARD.md README.md; do + test -f "$f" || { echo "::error::Missing required file: $f"; exit 1; } + done + + tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r orchestrator/requirements.txt -r agent/requirements.txt + + # Hermetic: in-memory store, planner forced offline by the tests — no model key needed. + - name: Manager contract test + env: { REDIS_FAKE: "1" } + run: python scripts/test-runtime-contract.py + + - name: Workflow mechanism smoke test + env: { REDIS_FAKE: "1" } + run: python scripts/test-merge-smoke.py + + - name: End-to-end workflow test + env: { REDIS_FAKE: "1" } + run: python scripts/test-workflow-e2e.py diff --git a/CLAUDE.md b/CLAUDE.md index 532ead4..777a22f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # CLAUDE.md — HeiCode Swarm(执行面 / 运行时) -本仓库(`agent_swarm_v5`,对应 **HeiCode-Swarm**)是蜂群执行面 / 回调 / Swarm Runtime。整体介绍见 [README.md](README.md),工程标准见 [PROJECT_STANDARD.md](PROJECT_STANDARD.md),交付说明见 [docs/DELIVERY.md](docs/DELIVERY.md)。 +本仓库(`agent_swarm_v6`,对应 **HeiCode-Swarm**)是蜂群执行面 / 回调 / Swarm Runtime。整体介绍见 [README.md](README.md),工程标准见 [PROJECT_STANDARD.md](PROJECT_STANDARD.md),交付说明见 [docs/DELIVERY.md](docs/DELIVERY.md)。 ## 改动前必读 1. 先读本 `CLAUDE.md` 与本仓 `PROJECT_STANDARD.md`。 diff --git a/CLIENT_GUIDE.md b/CLIENT_GUIDE.md new file mode 100644 index 0000000..888dbac --- /dev/null +++ b/CLIENT_GUIDE.md @@ -0,0 +1,123 @@ +# 客户端接入指南(Client Guide) + +本文说明**客户端如何与 Agent Swarm 运行时交互**。系统总览见 [README.md](README.md);接口契约见 [docs/integration/runtime-contract.md](docs/integration/runtime-contract.md);事件结构见 [docs/integration/event-schema.md](docs/integration/event-schema.md)。 + +> 架构说明:生产链路中,终端用户通过 **Heicode Manager / 桌面客户端** 提交需求,由 Manager 调用本 Swarm 运行时(见 `heicode-swarm-deferred` 裁定)。「客户端」在本文指**调用 Swarm 运行时 REST API 的一方**(Manager、联调脚本或开发者)。Swarm 不在对话回路,通过**带签名回调**回报状态。 + +--- + +## 1. 两个交互面 + +1. **REST API(客户端 → Swarm)**:创建部署、查询状态/任务/日志/事件/指标、审批、停止。 +2. **回调事件流(Swarm → 客户端/Manager)**:生命周期事件经带 HMAC 签名的回调推送(见 §6)。 + +WebSocket `/ws/{agent_id}` 仅供 **Agent 执行单元** 接入,**不是客户端接口**。 + +--- + +## 2. 鉴权 + +- 客户端调用带 `Authorization: Bearer `,Swarm 校验 `AGENT_RUNTIME_SERVICE_TOKEN`(兼容 `AGNET_RUNTIME_SERVICE_TOKEN`)。 +- 未配置令牌时为**非安全开发模式**(不校验,仅本地)。 +- 常用请求头:`X-Correlation-ID`(贯穿追踪)、`X-Idempotency-Key`(创建幂等)。 +- 响应信封:成功 `{ "success": true, "data": {...} }`;失败 `{ "success": false, "error": {code,message,request_id} }`。 + +--- + +## 3. 生命周期:创建 → 观察 → (审批)→ 停止 + +### 3.1 创建部署 +`POST /api/swarms`(别名 `/api/agent/swarm/deployments`、`/api/agnet/deployments`) + +```bash +curl -s -X POST http://:8000/api/swarms \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $TOKEN" \ + -H "X-Correlation-ID: corr-123" \ + -H "X-Idempotency-Key: idem-123" \ + -d '{ + "mode": "swarm", + "requirement": { "objective": "实现 add(a,b) 并补充测试与说明" }, + "callback": { "url": "https://your-manager/api/agent/callbacks/runtime-events", "subscribed_events": [] }, + "metadata": { "manager_deployment_id": "dep-1" } + }' +``` +必填:`requirement.objective`(或 `orchestration_plan.objective`)、`callback.url`、`metadata.manager_deployment_id`。 + +响应 `data`:`deployment_id`、`runtime_deployment_id`、`swarm_id`(=workflow_id)、`manager_deployment_id`、`mode`、`status`、`created`。**请保存 `deployment_id`** 用于后续查询。 + +### 3.2 观察 +| 接口 | 用途 | +|---|---| +| `GET /api/swarms/{deployment_id}` | 部署状态概要 | +| `GET /api/swarms/{deployment_id}/workflow` | 工作流/阶段视图(phases、agents、tokens、tools、artifacts、summary) | +| `GET /api/swarms/{deployment_id}/tasks` | 任务 DAG(task_id、agent_role、status、depends_on…) | +| `GET /api/swarms/{deployment_id}/logs`(`/events`) | 事件流,分页 `?limit=&cursor=`(断线后用 cursor 回补) | +| `GET /api/swarms/{deployment_id}/metrics` | 任务/Agent/预算指标 | +| `GET /api/swarms/{deployment_id}/diagnostics` | 失败、回调尝试、审批诊断 | + +状态机:`waiting_approval → running → (blocked ⇄ running) → completed | failed | stopped`。 + +### 3.3 审批(高危操作) +当部署进入 `waiting_approval` 并推送 `approval.requested` 时,客户端经 Manager 审批后回执: +```bash +curl -s -X POST http://:8000/api/swarms//approvals/ \ + -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ + -d '{ "decision": "approved" }' # 或 "rejected",可带 credential_ref / reason +``` + +### 3.4 停止 +```bash +curl -s -X POST http://:8000/api/swarms//stop \ + -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ + -d '{ "reason": "client requested stop" }' +``` + +--- + +## 4. 健康检查 + +- `GET /` → `{status, service}`;`GET /health` → 含 Redis 与连接数;`GET /metrics` → Prometheus。 + +--- + +## 5. 启用完整工作流 + +默认仅执行 Manager 提供的编排;要启用「分解 → 评审/重做 → 汇总」主控工作流,运行 Swarm 时设置(默认关闭): +- `ENABLE_PLANNER_FALLBACK=1`:无 Manager 分工时由主控 Agent 自动分解。 +- `ENABLE_REVIEW_LOOP=1` / `MAX_REVIEW_CYCLES=2`:主控评审与重做循环 + 结果汇总。 +- 模型走 OpenAI 兼容网关(`OPENAI_API_KEY`/`OPENAI_API_BASE`/`OPENAI_MODEL`,经 `secret_ref` 注入)。 + +--- + +## 6. 回调事件流(Swarm → 客户端/Manager) + +Swarm 向 `callback.url` POST 事件,**带 HMAC 签名**(客户端须验签): +- 头:`X-Agent-Timestamp`(ms)、`X-Agent-Signature`、`X-Agent-Event-Id`、`X-Correlation-ID`(+ `X-Agnet-` 兼容别名)。 +- 签名:`sha256=hex(HMAC_SHA256(secret, "{timestamp}.{event_id}.{raw_body}"))`,secret = `AGENT_CALLBACK_SIGNING_SECRET`,时间容差 300s。 +- 幂等:按 `X-Agent-Event-Id` → body `event_id` → `idempotency_key` 去重。 +- 事件类型与必填字段:见 [event-schema.md](docs/integration/event-schema.md)(`deployment.status_changed`、`task.*`、`handoff.*`、`approval.requested`、`artifact.created`、`timeline.updated`、`budget.alert`…)。 + +客户端可只消费 REST(轮询 `/workflow`+`/logs?cursor=`),或同时接收回调。 + +--- + +## 7. 本地联调(无需密钥) + +```bash +# 1) 编排器(内存存储 + 工作流开关) +set "REDIS_FAKE=1" & set "ENABLE_PLANNER_FALLBACK=1" & set "ENABLE_REVIEW_LOOP=1" +python -m uvicorn orchestrator.main:app --host 0.0.0.0 --port 8000 +# 2) 无密钥桩 Agent(联调用) +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 +# 3) 用 §3.1 的 curl 提交需求,再用 §3.2 观察 +``` + +--- + +## 8. 说明与边界 + +- **桌面/前端 UI**:当前桌面客户端为单 Agent A2A 直连模型;**蜂群图 / 协作时间线**的前端事件 API 尚未冻结(见 [frontend-event-api.md](docs/integration/frontend-event-api.md))。 +- **不得在请求/回调/日志中出现明文密钥**;凭据一律 `secret_ref`(`azkv://`)。见 [security-boundary.md](docs/integration/security-boundary.md)。 +- 计费在 NewAPI(模型网关)侧按请求计量,Swarm 仅上报用量;见 [usage-billing-schema.md](docs/integration/usage-billing-schema.md)。 diff --git a/README.md b/README.md index 0eed45c..a1e99d7 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,23 @@ # Agent Swarm(HeiCode Swarm) -一个多智能体「蜂群」系统:用户提出需求后,由主控逻辑自动将其分解为多个子任务,分发给擅长不同领域的专家 Agent 并行完成;专家之间可就重叠领域相互协作;产出汇总后由主控评审是否达标,未达标则退回重做,循环直至生成满意的最终回答。 +一个多智能体「蜂群」系统:用户提出需求后,由**主控 Agent(Master Agent,`orchestrator/master_agent.py`)**自动将其分解为多个子任务,分发给擅长不同领域的专家 Agent 并行完成;专家之间可就重叠领域相互协作;产出汇总后由主控 Agent 评审是否达标,未达标则退回重做,循环直至生成满意的最终回答。主控 Agent 负责「分解 / 评审决策 / 汇总」的认知决策,编排器负责执行其决策(派发、重开任务、持久化、事件)。 + +> ⚠️ **能力边界(主链路接入状态)** +> 本仓当前是一个**可运行的多 Agent 工作流运行系统**,**尚未**作为 Heicode 主链路的正式 Runtime Backend 接入。下表区分能力状态;契约见 [docs/integration/](docs/integration/),量化标准见 [docs/benchmark/](docs/benchmark/)。 +> +> | 能力 | 状态 | +> |---|---| +> | 任务分解(规划回退)、能力路由派发、专家执行(OpenAI 兼容)、评审/重做循环、结果汇总、peer 协作消息路由、WebSocket Agent 协议、Redis 持久化、Prometheus 指标 | ✅ 已实现(仓内可运行) | +> | Manager↔Runtime 生命周期契约(对齐 `heicode-am-contract`)、HMAC 签名回调事件 envelope、稳定的 `deployment_id`/`workflow_id`/`trace_id` | 🟡 待接入(见 `docs/integration/runtime-contract.md`;事件 envelope 与签名待 Manager 对齐) | +> | 统一 usage/计费聚合(归属 NewAPI)、审计/lineage trace、前端事件 API、统一 Agent registry/scheduling、统一 secret/workspace/tool/MCP/tenant 安全边界 | 🟡 待接入(需与 Billing / Audit / Frontend / Infra / Security Team 对齐) | +> | Benchmark 自证(`Benchmark_Agent`、`S_swarm`、`G_E`、`G_E,c`、治理/协作/通信/鲁棒性指标、baseline 对比、telemetry 架构) | 🔴 规划中(标准见 `docs/benchmark/`,采集器尚未落地) | +> +> 在以上「待接入 / 规划中」项目完成并经对应 Team 验收前,本文与各子文档**不得宣称**「已接入主链路」或「已具备完整 Agent Swarm 工程能力」。 ## 系统架构 +> 说明:下图中 **Heicode Manager → Orchestrator** 为**目标形态**。当前 orchestrator 仅实现了占位的 Manager 面接口与回调骨架,尚未按 `heicode-am-contract` 正式注册为 Runtime Backend(见 `docs/integration/runtime-contract.md`)。 + ``` 用户 │ diff --git a/agent/main.py b/agent/main.py index a89084a..b56f419 100644 --- a/agent/main.py +++ b/agent/main.py @@ -74,6 +74,7 @@ class Agent: MAX_CONCURRENT_TASKS = int(os.getenv("MAX_CONCURRENT_TASKS", "4")) TASK_TIMEOUT_SECONDS = int(os.getenv("TASK_TIMEOUT_SECONDS", "60")) + PEER_REPLY_TIMEOUT_SECONDS = int(os.getenv("PEER_REPLY_TIMEOUT_SECONDS", "20")) HEARTBEAT_INTERVAL_SECONDS = 15 def __init__( @@ -102,6 +103,8 @@ class Agent: self.peer_waiters: dict[str, asyncio.Future] = {} # Summary of this agent's most recently completed task, shared when peers consult it. self.last_summary: Optional[str] = None + # Lazily-created executor used to compose substantive peer replies. + self._peer_executor: Optional[TaskExecutor] = None self.workspace_git = GitOperations(str(self.workspace_dir), self.agent_id) @@ -233,28 +236,66 @@ class Agent: summary = result.get("summary") return summary.strip() if isinstance(summary, str) and summary.strip() else None + def _get_peer_executor(self) -> TaskExecutor: + # Lazily create a TaskExecutor for peer replies (reuses the model client / workspace). + # Raises if no model key is configured; callers fall back to the cached summary. + if self._peer_executor is None: + self._peer_executor = TaskExecutor(agent_id=self.agent_id, workspace_dir=str(self.workspace_dir)) + return self._peer_executor + + def _peer_fallback_reply(self) -> dict: + # Cheap, no-LLM reply used when the model is unavailable or errors. + shared = ( + f"My latest result: {self.last_summary}. " + if self.last_summary + else "No completed result yet. " + ) + content = ( + f"From {self.agent_id} (capabilities: {', '.join(self.capabilities)}). {shared}" + "Treat implementation artifacts as the source of truth for behavior and exception semantics." + ) + return {"content": content, "stance": "info", "evidence": self.last_summary or "", "refs": []} + + async def _build_peer_reply(self, query: str, task_id: Optional[str]) -> dict: + # Produce a substantive, query-scoped reply grounded in this agent's own work. + # Falls back to the cached summary if there is no query, no model key, or the call fails. + if not query: + return self._peer_fallback_reply() + try: + executor = self._get_peer_executor() + except Exception as e: + logger.warning(f"peer reply executor unavailable ({e}); using cached summary") + return self._peer_fallback_reply() + try: + return await asyncio.wait_for( + executor.peer_reply( + query=query, + capabilities=self.capabilities, + last_summary=self.last_summary, + ), + timeout=self.PEER_REPLY_TIMEOUT_SECONDS, + ) + except Exception as e: + logger.warning(f"peer reply LLM failed ({e}); using cached summary") + return self._peer_fallback_reply() + async def answer_peer_query(self, message: dict): - # Respond to an inbound peer query, sharing what this agent has actually done so far. + # Respond to an inbound peer query with a substantive, grounded reply (LLM, with fallback). requester = message.get("from_agent_id") or message.get("agent_id") correlation_id = message.get("correlation_id") if not requester or not correlation_id: return - shared = ( - f"My latest result: {self.last_summary}" - if self.last_summary - else "I have no completed result to share yet." - ) - reply_content = ( - f"From {self.agent_id} (capabilities: {', '.join(self.capabilities)}). {shared} " - "Treat implementation artifacts as the source of truth for behavior and exception semantics." - ) + reply = await self._build_peer_reply((message.get("content") or "").strip(), message.get("task_id")) try: await self.safe_send({ "type": "peer_message", "agent_id": self.agent_id, "target_agent_id": requester, "task_id": message.get("task_id"), - "content": reply_content, + "content": reply.get("content", ""), + "stance": reply.get("stance"), + "evidence": reply.get("evidence"), + "refs": reply.get("refs"), "correlation_id": correlation_id, "is_reply": True, "timestamp": time.time(), @@ -489,7 +530,8 @@ class Agent: message.get("task_id"), message.get("from_agent_id") or message.get("agent_id"), ) - await self.answer_peer_query(message) + # Answer in the background so a slow (LLM) reply doesn't stall the message loop. + asyncio.create_task(self.answer_peer_query(message)) async def handle_message(self, message: dict): # Route each inbound orchestrator message to the appropriate handler. diff --git a/agent/task_executor.py b/agent/task_executor.py index c32178c..99e4795 100644 --- a/agent/task_executor.py +++ b/agent/task_executor.py @@ -292,6 +292,44 @@ Return ONLY the JSON, no other text.""" self._record_openai_usage(response) return response.choices[0].message.content or "" + async def peer_reply(self, *, query: str, capabilities: list[str], + last_summary: Optional[str], max_tokens: Optional[int] = None) -> dict: + """Compose a substantive, grounded reply to a peer agent's query (one bounded LLM call). + + Returns {stance, content, evidence, refs}. `content` is what the requesting agent reads. + Bounded by PEER_CONSULT_MAX_TOKENS (default 500). + """ + if max_tokens is None: + max_tokens = int(os.getenv("PEER_CONSULT_MAX_TOKENS", "500")) + workspace_files = self._summarize_workspace() + workspace_context = self._collect_workspace_context(max_files=8) + prompt = f"""You are a specialist agent being consulted by a peer in a collaborative swarm. +Your capabilities: {', '.join(capabilities)} +Your latest completed work (summary): {last_summary or 'none'} + +A peer asks: +{query} + +Your workspace files: +{json.dumps(workspace_files, indent=2)} +Relevant workspace file contents: +{json.dumps(workspace_context, indent=2)} + +Answer concisely and concretely, grounded in YOUR actual work/artifacts. Treat implementation +artifacts as the source of truth for behavior and exception semantics; if the peer's assumption +conflicts with your work, say so. + +Return ONLY JSON: +{{\"stance\": \"agree|disagree|info\", \"content\": \"\", \"evidence\": \"\", \"refs\": [\"relative/file/path\"]}}""" + content = await self._complete(prompt, max_tokens=max_tokens) + result = self._parse_json_response(content) + if not result.get("content"): + result["content"] = (content or "").strip()[:1000] + result.setdefault("stance", "info") + result.setdefault("evidence", "") + result.setdefault("refs", []) + return result + def _empty_usage(self, context: Optional[dict] = None) -> dict: plan = ((context or {}).get("orchestration_plan") or {}) billing = plan.get("billing_context") or {} diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 0000000..75ca00d --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,22 @@ +# benchmark/ —— 蜂群基准(骨架) + +对应 [`docs/benchmark/`](../docs/benchmark/) 的标准实现入口。依据 **Agent 蜂群指标量化与标准 v2.0**。 + +## 实现状态 + +| 模块 | 内容 | 状态 | +|---|---|---| +| `metrics.py` | v2.0 `SwarmMetrics`(15 字段)+ **纯公式**(`pheromone`/`heuristic`/`p_decision`/`reward`/`completion`/`collaboration`/`communication`/`cost`/`robustness`/`governance`/`swarm_score`/`emergence_gain`/`swarm_cost`/`cost_normalized_gain`/`benchmark_agent`)+ v2.0 推荐权重常量 | ✅ 已实现、可单测(`test-benchmark-metrics.py`) | +| `collectors/` | base `SwarmMetricsCollector` + `SwarmRunMetricsCollector`(从真实 run 计算 `s_completion`/`s_collaboration`/`s_cost`/`s_robustness`;其余标记 NaN + `coverage=False`) | 🟡 部分落地(4/15 字段真实可算) | +| `baselines/` | Single / Chain / Sub-Agent / Strong 基线运行器 | 🔴 未落地 | +| `replay/` | 执行回放 | 🔴 未落地 | +| `leaderboard/` | 排行榜聚合 | 🔴 未落地 | + +> 公式可计算 ≠ 能自证。**数据采集(collectors)与基线(baselines)未落地前,不得宣称已具备 +> Agent Swarm 量化自证能力**(不能证明 `Swarm > Single/Chain/Sub-Agent/Strong`,也无法输出完整 `Benchmark_Agent`)。 + +## 待落地(按依赖顺序) +1. `collectors/`:接入事件流 / Prometheus / OTel / 任务与用量(telemetry-architecture)。 +2. `baselines/`:四类基线 + 统一任务集(emergence-evaluation / baseline-comparison)。 +3. `replay/`、`leaderboard/`。 +4. 权重 v2.0 已给定(`λ`/`w*`/`γ*`,Σλ=1.0);`α/β/ρ/N_agent/ε` 用 `THETA_DEFAULTS` 推荐初值,调优口径待定。 diff --git a/benchmark/__init__.py b/benchmark/__init__.py new file mode 100644 index 0000000..6f65eef --- /dev/null +++ b/benchmark/__init__.py @@ -0,0 +1,8 @@ +"""Agent Swarm benchmark package. + +实现状态(见 docs/benchmark/swarm-benchmark-protocol.md §6): +- metrics.py:指标公式(纯函数,已实现、可测)+ SwarmMetrics 数据类。 +- collectors/ baselines/ replay/ leaderboard/:骨架,数据采集与基线运行**未落地**。 + +在采集器与基线运行器落地前,不得宣称本仓具备 Agent Swarm 量化自证能力。 +""" diff --git a/benchmark/baselines/__init__.py b/benchmark/baselines/__init__.py new file mode 100644 index 0000000..e814142 --- /dev/null +++ b/benchmark/baselines/__init__.py @@ -0,0 +1,14 @@ +"""Benchmark baselines — Single / Chain / Sub-Agent / Strong(v2.0 §9.1)。 + +两部分: +- **比较评估器(已落地)**:`comparison.py` 定义共享记录 `BenchmarkRunRecord` 与 `compare`/`evaluate`, + 在给定 swarm 与 baseline 记录时计算 `G_E` / `G_E,c`(纯函数、可单测)。 +- **基线运行器(未落地,🔴)**:实际运行各基线系统、在统一任务集上产出 `BenchmarkRunRecord` 的 runner, + 以及对应的 Quality 插桩(TestPass/CodeReview/UserAcceptance),尚未实现。 + +即:一旦 swarm 与 sub-agent 程序按 `BenchmarkRunRecord` 产出记录,比较即可计算。 +记录的产生(尤其 Quality)见 docs/benchmark/baseline-record-schema.md。 +""" +from .comparison import ( # noqa: F401 + BenchmarkRunRecord, SYSTEMS, quality, cost_efficiency, unified_metrics, compare, evaluate, +) diff --git a/benchmark/baselines/comparison.py b/benchmark/baselines/comparison.py new file mode 100644 index 0000000..3203625 --- /dev/null +++ b/benchmark/baselines/comparison.py @@ -0,0 +1,123 @@ +"""Baseline comparison adapter — Agent 蜂群指标量化与标准 v2.0. + +Defines the shared `BenchmarkRunRecord` that BOTH the swarm and each baseline (Single / Chain / +Sub-Agent / Strong) emit per benchmark run, plus the evaluator that computes G_E and G_E,c from +two records. Pure/deterministic given records — the records themselves must be PRODUCED by the +respective programs (instrumentation, esp. Quality, is pending; see +docs/benchmark/baseline-record-schema.md). No record → no comparison (we never fabricate scores). +""" +from __future__ import annotations + +from dataclasses import dataclass + +from ..metrics import ( + quality_score, completion_score, speed_score, cost_score, robustness_score, + emergence_gain, swarm_cost, cost_normalized_gain, BASE_COEFFICIENTS, +) + +SYSTEMS = {"swarm", "single", "chain", "sub", "strong"} + + +@dataclass +class BenchmarkRunRecord: + """Unified per-run record (v2.0 §9 collection). Both swarm and baselines emit this.""" + system: str # one of SYSTEMS + scenario: str # coding | refactoring | architecture | devops | bugfix + task_set_id: str # identifies the shared task set (fairness: same id across systems) + n_agent: int # agent count used by this system + completed_tasks: int + total_tasks: int + test_pass_rate: float # 0..100 (Quality input) + code_review_score: float # 0..100 (Quality input) + user_acceptance: float # 0..100 (Quality input) + budget_usd: float # planned cost + actual_cost_usd: float # measured model cost + model_tokens: int + target_time_s: float # planned/target time + actual_time_s: float # measured time + recovered_failures: int + total_failures: int + + @classmethod + def from_dict(cls, data: dict) -> "BenchmarkRunRecord": + missing = [f for f in cls.__dataclass_fields__ if f not in data] + if missing: + raise ValueError(f"benchmark record missing required fields: {missing}") + if data["system"] not in SYSTEMS: + raise ValueError(f"unknown system '{data['system']}' (expected one of {sorted(SYSTEMS)})") + return cls(**{f: data[f] for f in cls.__dataclass_fields__}) + + +# --- derived quantities (from a single record) --- +def quality(rec: BenchmarkRunRecord) -> float: + """Q = 0.4·TestPass + 0.3·CodeReview + 0.3·UserAcceptance (v2.0 §4).""" + return quality_score(rec.test_pass_rate, rec.code_review_score, rec.user_acceptance) + + +def cost_efficiency(rec: BenchmarkRunRecord) -> float: + """CostEfficiency = 100·Budget/ActualCost (v2.0 §4 E_cost口径).""" + return cost_score(rec.budget_usd, rec.actual_cost_usd) + + +def unified_metrics(rec: BenchmarkRunRecord) -> dict: + """The protocol's unified collection (v2.0 §9): Completion / Quality / Cost / Time / Robustness.""" + return { + "completion": completion_score(rec.completed_tasks, rec.total_tasks), + "quality": quality(rec), + "cost_efficiency": cost_efficiency(rec), + "speed": speed_score(rec.target_time_s, rec.actual_time_s), + "robustness": robustness_score(rec.recovered_failures, rec.total_failures), + } + + +# --- comparison (swarm vs one baseline) --- +def compare(swarm_rec: BenchmarkRunRecord, base_rec: BenchmarkRunRecord, *, + symmetric_cost: bool = True) -> dict: + """Compute G_E and G_E,c for the swarm against one baseline record. + + G_E = Q_swarm − Q_base + G_E,c = (Q_swarm / C_swarm) − (Q_base / C_base), C_swarm = N_agent·(CostEfficiency/100+0.5) + + `symmetric_cost`: + - True (DEFAULT — Benchmark Owner ratified correction, 2026-06-09): `C_base` uses the SAME + formula as C_swarm with the baseline's own N_agent + CostEfficiency → both terms are + quality-per-cost (comparable, non-degenerate). + - False: literal v2.0 text — `C_base = 1.0` (scale-degenerate; kept for reference only). + See docs/benchmark/cost-normalized-gain.md and OWNER-NOTE-cost-normalized-gain.md. + """ + if swarm_rec.system != "swarm": + raise ValueError("swarm_rec.system must be 'swarm'") + if swarm_rec.task_set_id != base_rec.task_set_id: + raise ValueError("records must share the same task_set_id (fair comparison)") + + q_swarm, q_base = quality(swarm_rec), quality(base_rec) + g_e = emergence_gain(q_swarm, q_base) + c_swarm = swarm_cost(swarm_rec.n_agent, cost_efficiency(swarm_rec)) + c_base = swarm_cost(base_rec.n_agent, cost_efficiency(base_rec)) if symmetric_cost else 1.0 + g_e_c = cost_normalized_gain(q_swarm, c_swarm, q_base, c_base=c_base) + return { + "base_system": base_rec.system, + "base_coefficient": BASE_COEFFICIENTS.get(base_rec.system), + "cost_mode": "symmetric" if symmetric_cost else "literal_v2.0", + "q_swarm": q_swarm, + "q_base": q_base, + "c_swarm": c_swarm, + "c_base": c_base, + "g_e": g_e, + "g_e_cost": g_e_c, + "raw_gain_positive": g_e > 0, + "cost_normalized_positive": g_e_c > 0, + } + + +def evaluate(swarm_rec: BenchmarkRunRecord, baseline_recs: list[BenchmarkRunRecord], *, + symmetric_cost: bool = True) -> dict: + """Compare the swarm against all baselines. swarm_valid requires beating ALL on G_E and G_E,c.""" + results = [compare(swarm_rec, b, symmetric_cost=symmetric_cost) for b in baseline_recs] + swarm_valid = bool(results) and all(r["raw_gain_positive"] and r["cost_normalized_positive"] for r in results) + return { + "scenario": swarm_rec.scenario, + "cost_mode": "symmetric" if symmetric_cost else "literal_v2.0", + "comparisons": results, + "swarm_valid": swarm_valid, + } diff --git a/benchmark/collectors/__init__.py b/benchmark/collectors/__init__.py new file mode 100644 index 0000000..c5b2ca5 --- /dev/null +++ b/benchmark/collectors/__init__.py @@ -0,0 +1,20 @@ +"""Benchmark collectors — 把运行时信号采集成 SwarmMetrics。 + +状态:**部分落地**。base `SwarmMetricsCollector.collect()` 故意抛 NotImplementedError(避免占位冒充完成, +组织规则 #9);具体实现 `run_collector.SwarmRunMetricsCollector` 从真实 run 计算 +`completion`/`collaboration`/`cost`/`robustness`,其余指标返回 NaN 并在 `coverage` 标记 False。 +未落地部分(gain/communication/p_decision/reward + 基线)见 docs/benchmark/telemetry-architecture.md 与 baseline-comparison.md。 +""" +from __future__ import annotations + +from ..metrics import SwarmMetrics + + +class SwarmMetricsCollector: + """标准 §9 采集接口。子类应从运行时数据源装配 SwarmMetrics。""" + + async def collect(self) -> SwarmMetrics: + raise NotImplementedError( + "数据采集未落地:需接入 telemetry-architecture 的数据源后," + "用 benchmark.metrics 的公式装配 SwarmMetrics。" + ) diff --git a/benchmark/collectors/run_collector.py b/benchmark/collectors/run_collector.py new file mode 100644 index 0000000..8cb3292 --- /dev/null +++ b/benchmark/collectors/run_collector.py @@ -0,0 +1,137 @@ +"""SwarmRunMetricsCollector — compute SwarmMetrics from a real swarm run. + +Wires benchmark.metrics formulas to a run's tasks + event stream (orchestrator state). +Only metrics with real data sources are computed; the rest are returned as NaN and flagged +in `coverage` (False) — we do NOT fake a 0/100 score for uncollected metrics (rule #9). + +Uncollected today (need work flagged in docs/benchmark/swarm-metrics-schema.md): +- gain → needs baselines (emergence-evaluation) +- communication → needs agent message telemetry (not counted yet) +- p_decision → needs τ/η decision scoring (not implemented) +- reward → needs weights (standard gives no numeric w*) +- governance → only derivable from approvals; NaN when a run has no governed ops +""" +from __future__ import annotations + +import json +import math +from collections import Counter + +from . import SwarmMetricsCollector +from ..metrics import ( + SwarmMetrics, completion_score, collaboration_score, cost_score, robustness_score, + governance_score, +) + + +def _task_cost(task) -> float: + """Extract model_cost_usd from a task's stored result, 0.0 if absent.""" + result = getattr(task, "result", None) + if not result: + return 0.0 + try: + data = json.loads(result) if isinstance(result, str) else result + return float((data.get("usage") or {}).get("model_cost_usd") or 0.0) + except Exception: + return 0.0 + + +class SwarmRunMetricsCollector(SwarmMetricsCollector): + """Collect SwarmMetrics for one swarm run (by swarm_id).""" + + def __init__(self, swarm_id: str): + self.swarm_id = swarm_id + self.coverage: dict[str, bool] = {} + + async def collect(self) -> SwarmMetrics: + # Lazy import: the collector reads live orchestrator state. + from orchestrator.swarm_runtime import swarm_runtime + from orchestrator.task_queue import task_queue, TaskStatus + + run = await swarm_runtime.get_run(self.swarm_id) + if not run: + raise ValueError(f"run not found: {self.swarm_id}") + + tasks = [t for t in [await task_queue.get_task(tid) for tid in run.task_ids] if t] + events = (await swarm_runtime.list_events(self.swarm_id, limit=10000)).get("events", []) + event_types = Counter(e.get("event_type") for e in events) + + def status(t): + return t.status.value if hasattr(t.status, "value") else t.status + + # --- s_completion (real) --- + total = len(tasks) + completed = sum(1 for t in tasks if status(t) == TaskStatus.COMPLETED.value) + s_completion = completion_score(completed, total) + self.coverage["s_completion"] = total > 0 + + # --- s_collaboration (real) --- + req = event_types.get("handoff.requested", 0) + comp = event_types.get("handoff.completed", 0) + handoff_success = (100.0 * comp / req) if req else 100.0 + dep_tasks = [t for t in tasks if t.depends_on] + done_ids = {t.task_id for t in tasks if status(t) == TaskStatus.COMPLETED.value} + resolved = [t for t in dep_tasks if all(d in done_ids for d in t.depends_on)] + dep_resolution = (100.0 * len(resolved) / len(dep_tasks)) if dep_tasks else 100.0 + per_agent = Counter(t.assigned_agent_id for t in tasks if t.assigned_agent_id) + if per_agent: + counts = list(per_agent.values()) + workload_balance = 100.0 * (min(counts) / max(counts)) + else: + workload_balance = 100.0 + s_collaboration = collaboration_score(handoff_success, dep_resolution, workload_balance) + self.coverage["s_collaboration"] = total > 0 + + # --- s_robustness (real) --- + failures = [t for t in tasks if t.retry_count > 0 or status(t) == TaskStatus.FAILED.value] + recovered = [t for t in failures if status(t) == TaskStatus.COMPLETED.value] + s_robustness = robustness_score(len(recovered), len(failures)) + self.coverage["s_robustness"] = True + + # --- s_cost (real if budget + usage present) --- + plan = (run.request_body or {}).get("orchestration_plan") or {} + budget = plan.get("budget") or {} + max_cost = budget.get("max_cost_usd") + actual = sum(_task_cost(t) for t in tasks) + if isinstance(max_cost, (int, float)) and actual > 0: + s_cost = cost_score(float(max_cost), actual) + self.coverage["s_cost"] = True + else: + s_cost = math.nan + self.coverage["s_cost"] = False + + # --- s_governance (real only if the run had governed ops / approvals) --- + approvals = list((run.approvals or {}).values()) + if approvals: + compliant = sum(1 for a in approvals if a.get("decision") in ("approved", "rejected")) + s_governance = governance_score(compliant, len(approvals)) + self.coverage["s_governance"] = True + else: + s_governance = math.nan + self.coverage["s_governance"] = False + + # --- not yet collectable (see docs/benchmark/metric-coverage-gaps.md) --- + # s_gain needs baselines; s_communication needs message telemetry; tau/eta/p_decision need + # the decision-layer signals; reward needs quality/risk/rework inputs; s_swarm/g_e/g_e_cost/ + # benchmark depend on the above (any NaN component → NaN aggregate). + for k in ("tau", "eta", "p_decision", "reward", "s_gain", "s_communication", + "s_swarm", "g_e", "g_e_cost", "benchmark"): + self.coverage[k] = False + + return SwarmMetrics( + tau=math.nan, + eta=math.nan, + p_decision=math.nan, + reward=math.nan, + s_completion=s_completion, + s_gain=math.nan, + s_collaboration=s_collaboration, + s_communication=math.nan, + s_cost=s_cost, + s_robustness=s_robustness, + s_governance=s_governance, + s_swarm=math.nan, + g_e=math.nan, + g_e_cost=math.nan, + benchmark=math.nan, + ) diff --git a/benchmark/leaderboard/__init__.py b/benchmark/leaderboard/__init__.py new file mode 100644 index 0000000..c6a1707 --- /dev/null +++ b/benchmark/leaderboard/__init__.py @@ -0,0 +1,6 @@ +"""Benchmark leaderboard — 排行榜聚合与展示字段。 + +状态:**未落地(骨架)**。展示字段(标准 §11):Benchmark_Agent / S_swarm / G_E / G_E,c / +Reward / Cost Efficiency;场景:Coding / Refactoring / Architecture / DevOps / Bug Fix。 +需定义 leaderboard schema 与持久化。 +""" diff --git a/benchmark/metrics.py b/benchmark/metrics.py new file mode 100644 index 0000000..8889d84 --- /dev/null +++ b/benchmark/metrics.py @@ -0,0 +1,177 @@ +"""Swarm benchmark metric formulas + SwarmMetrics schema. + +Aligned to **Agent 蜂群指标量化与标准 v2.0**. Pure formulas only (given inputs → value, unit-testable); +data collection (turning runtime signals into inputs) is partial — see collectors/run_collector.py +and docs/benchmark/metric-coverage-gaps.md. Recommended weights/hyperparameters from v2.0 are +provided as defaults below. +""" +from __future__ import annotations + +from dataclasses import dataclass + +# --- v2.0 recommended weights / hyperparameters --- +TAU_WEIGHTS = {"success": 0.25, "quality": 0.20, "acceptance": 0.20, + "cost": 0.10, "time": 0.10, "risk": 0.08, "rollback": 0.07} +ETA_WEIGHTS = {"match": 0.25, "urgency": 0.15, "dependency": 0.15, "resource": 0.15, + "confidence": 0.10, "risk": 0.10, "budget_pressure": 0.10} +REWARD_WEIGHTS = {"s_task": 0.20, "q_quality": 0.20, "v_speed": 0.12, "e_cost": 0.13, + "r_robust": 0.13, "g_gov": 0.10, "p_risk": 0.07, "p_rework": 0.05} +SWARM_WEIGHTS = {"completion": 0.25, "gain": 0.20, "collaboration": 0.15, "communication": 0.10, + "cost": 0.10, "robustness": 0.10, "governance": 0.10} +LAMBDA_WEIGHTS = {"lambda1": 0.30, "lambda2": 0.20, "lambda3": 0.25, "lambda4": 0.15, "lambda5": 0.10} # Σ = 1.0 +THETA_DEFAULTS = {"alpha": 1.0, "beta": 2.0, "rho": 0.10, "n_agent": 5, "epsilon": 0.10} +# Q_base reference coefficients (standard §6.1), by baseline type. +# ⚠️ PROVISIONAL / METADATA ONLY: these are assumed constants — the standard gives NO derivation. +# They are reported as `base_coefficient` but used in NO formula (G_E/G_E,c use raw Q_base). +# TODO(benchmark): quantify empirically later, e.g. coefficient = Q_baseline / Q_reference on the +# shared task set (reference = strongest baseline or an oracle/acceptance ceiling). Until then, +# do not wire into scoring. See docs/benchmark/baseline-comparison.md §1.1. +BASE_COEFFICIENTS = {"single": 0.75, "chain": 0.85, "sub": 0.90, "strong": 0.95} + + +@dataclass +class SwarmMetrics: + """v2.0 §8.1 collection interface.""" + tau: float # 信息素得分 + eta: float # 启发式得分 + p_decision: float # 决策概率 + reward: float # 执行奖励 + s_completion: float + s_gain: float + s_collaboration: float + s_communication: float + s_cost: float + s_robustness: float + s_governance: float + s_swarm: float # 蜂群总分 + g_e: float # 涌现增益 + g_e_cost: float # 成本归一化增益 + benchmark: float # 综合评分 + + +def _ratio_pct(numerator: float, denominator: float, *, empty: float = 0.0) -> float: + if not denominator: + return empty + return 100.0 * numerator / denominator + + +# --- 决策层(v2.0 §3) --- +def pheromone(*, success: float, quality: float, acceptance: float, cost: float, time: float, + risk: float, rollback: float, weights: dict = TAU_WEIGHTS) -> float: + w = weights + return (w["success"] * success + w["quality"] * quality + w["acceptance"] * acceptance + - w["cost"] * cost - w["time"] * time - w["risk"] * risk - w["rollback"] * rollback) + + +def heuristic(*, match: float, urgency: float, dependency: float, resource: float, + confidence: float, risk: float, budget_pressure: float, weights: dict = ETA_WEIGHTS) -> float: + g = weights + return (g["match"] * match + g["urgency"] * urgency + g["dependency"] * dependency + + g["resource"] * resource + g["confidence"] * confidence + - g["risk"] * risk - g["budget_pressure"] * budget_pressure) + + +def action_probability(tau: float, eta: float, alpha: float, beta: float, + candidates: list[tuple[float, float]]) -> float: + # P(s,a,r) = τ^α·η^β / Σ τ_i^α·η_i^β + denom = sum((t ** alpha) * (e ** beta) for t, e in candidates) + if not denom: + return 0.0 + return ((tau ** alpha) * (eta ** beta)) / denom + + +def p_decision(tau: float, eta: float, alpha: float = THETA_DEFAULTS["alpha"], + beta: float = THETA_DEFAULTS["beta"]) -> float: + # v2.0 §3.3: P_decision = τ^α · η^β · 100 + return (tau ** alpha) * (eta ** beta) * 100.0 + + +# --- 执行层(v2.0 §4) --- +def quality_score(test_pass_rate: float, code_review_score: float, user_acceptance: float) -> float: + return 0.4 * test_pass_rate + 0.3 * code_review_score + 0.3 * user_acceptance + + +def speed_score(target_time: float, actual_time: float) -> float: + return _ratio_pct(target_time, actual_time) + + +def cost_efficiency_score(expected_cost: float, actual_cost: float) -> float: + # Owner ruling (v2.1): E_cost (§4.1) == S_cost (§5.1) == CostEfficiency (§6.2) — one quantity, + # 100×Budget/ActualCost (Budget≡ExpectedCost, ActualUsage≡ActualCost). Same math as cost_score(). + return _ratio_pct(expected_cost, actual_cost) + + +def rework_penalty(rework_count: int, total_tasks: int) -> float: + return _ratio_pct(rework_count, total_tasks) + + +def reward(*, s_task: float, q_quality: float, v_speed: float, e_cost: float, r_robust: float, + g_gov: float, p_risk: float, p_rework: float, weights: dict = REWARD_WEIGHTS) -> float: + w = weights + return (w["s_task"] * s_task + w["q_quality"] * q_quality + w["v_speed"] * v_speed + + w["e_cost"] * e_cost + w["r_robust"] * r_robust + w["g_gov"] * g_gov + - w["p_risk"] * p_risk - w["p_rework"] * p_rework) + + +# --- 蜂群层(v2.0 §5) --- +def completion_score(completed_tasks: int, total_tasks: int) -> float: + return _ratio_pct(completed_tasks, total_tasks) + + +def collaboration_score(handoff_success_rate: float, dependency_resolution_rate: float, + workload_balance_score: float) -> float: + return (0.5 * handoff_success_rate + 0.3 * dependency_resolution_rate + + 0.2 * workload_balance_score) + + +def communication_score(successful_messages: int, total_messages: int) -> float: + return _ratio_pct(successful_messages, total_messages) + + +def cost_score(budget: float, actual_usage: float) -> float: + return _ratio_pct(budget, actual_usage) + + +def robustness_score(recovered_failures: int, total_failures: int) -> float: + return _ratio_pct(recovered_failures, total_failures, empty=100.0) + + +def governance_score(compliant_operations: int, total_operations: int) -> float: + return _ratio_pct(compliant_operations, total_operations, empty=100.0) + + +def swarm_score(*, completion: float, gain: float, collaboration: float, communication: float, + cost: float, robustness: float, governance: float, weights: dict = SWARM_WEIGHTS) -> float: + w = weights + return (w["completion"] * completion + w["gain"] * gain + w["collaboration"] * collaboration + + w["communication"] * communication + w["cost"] * cost + + w["robustness"] * robustness + w["governance"] * governance) + + +# --- 涌现增益(v2.0 §6) --- +def emergence_gain(q_swarm: float, q_base: float) -> float: + # G_E = Q_swarm − Q_base + return q_swarm - q_base + + +def swarm_cost(n_agent: int, cost_efficiency: float) -> float: + # v2.0 §6.2: C_swarm = N_agent × (CostEfficiency/100 + 0.5) + return n_agent * (cost_efficiency / 100.0 + 0.5) + + +def cost_normalized_gain(q_swarm: float, c_swarm: float, q_base: float, c_base: float = 1.0) -> float: + # v2.0 §6.2 (CHANGED from v1 ratio-of-ratios to a DIFFERENCE): + # G_E,c = (Q_swarm / C_swarm) − (Q_base / C_base) + if not c_swarm or not c_base: + raise ValueError("cost_normalized_gain needs non-zero C_swarm / C_base") + return (q_swarm / c_swarm) - (q_base / c_base) + + +# --- 综合 Benchmark(v2.0 §7) --- +def benchmark_agent(*, s_swarm: float, g_e: float, reward: float, observability: float, + governance: float, weights: dict = LAMBDA_WEIGHTS, validate: bool = True) -> float: + """Benchmark_Agent = λ1·S_swarm + λ2·G_E + λ3·R + λ4·O + λ5·Gov, with Σλ = 1.0 (v2.0 §7.1).""" + if validate and abs(sum(weights.values()) - 1.0) > 1e-6: + raise ValueError(f"λ weights must sum to 1.0 (got {sum(weights.values())})") + return (weights["lambda1"] * s_swarm + weights["lambda2"] * g_e + weights["lambda3"] * reward + + weights["lambda4"] * observability + weights["lambda5"] * governance) diff --git a/benchmark/replay/__init__.py b/benchmark/replay/__init__.py new file mode 100644 index 0000000..712c8b2 --- /dev/null +++ b/benchmark/replay/__init__.py @@ -0,0 +1,5 @@ +"""Benchmark replay — 回放一次 swarm/benchmark 执行。 + +状态:**未落地(骨架)**。事件流已按 swarm_id 顺序持久化,可作为回放数据源; +需定义快照格式与回放接口。见 docs/integration/audit-trace-schema.md §4。 +""" diff --git a/docs/benchmark/IMPORTANT-metric-coverage-gaps.md b/docs/benchmark/IMPORTANT-metric-coverage-gaps.md new file mode 100644 index 0000000..47f0d81 --- /dev/null +++ b/docs/benchmark/IMPORTANT-metric-coverage-gaps.md @@ -0,0 +1,57 @@ +# 指标采集覆盖与缺口根因分析(Metric Coverage Gaps) + +> 状态:**现状分析**。说明 `SwarmRunMetricsCollector`(`benchmark/collectors/run_collector.py`)当前能真实计算哪些指标、哪些返回 `NaN + coverage=False`,以及**为什么**。 +> +> 配套:[`swarm-metrics-schema.md`](./swarm-metrics-schema.md)、[`telemetry-architecture.md`](./telemetry-architecture.md)、[`emergence-evaluation.md`](./emergence-evaluation.md)、[`baseline-comparison.md`](./baseline-comparison.md)、[`governance-score.md`](./governance-score.md)。 + +## 1. 一句话根因 + +本仓最初是**多 Agent 工作流执行器**,不是**被插桩的基准测量目标**。能算出来的指标是「执行过程本就会产生」的副产物;其余指标各自缺少执行器从不需要产生的东西(基线 / 计数器 / 决策机制 / 输入)。**v2.0 已给定全部权重(τ/η/reward/λ)**,但这不改变「输入/机制缺失」的根因。 + +## 2. 覆盖现状(v2.0 `SwarmMetrics` 共 15 字段,4 个真实可算) + +| 字段 | 状态 | 数据来源 / 缺口 | +|---|---|---| +| `s_completion` | ✅ 真实 | 任务状态统计 | +| `s_collaboration` | ✅ 真实 | `handoff.*` 事件 + `depends_on` + `assigned_agent_id` | +| `s_cost` | ✅ 真实 | 每任务 `usage.model_cost_usd` + 请求 `budget.max_cost_usd` | +| `s_robustness` | ✅ 真实 | `retry_count` + 任务状态 | +| `s_governance` | 🟡 部分 | 仅审批可派生;无审批 → NaN | +| `s_gain` | 🔴 NaN | 需基线 | +| `s_communication` | 🔴 NaN | 无消息计数 | +| `tau` / `eta` / `p_decision` | 🔴 NaN | 无 τ/η 决策引擎 | +| `reward` | 🔴 NaN | 权重已给定,**输入**未采集 | +| `s_swarm` / `g_e` / `g_e_cost` / `benchmark` | 🔴 NaN | 依赖上述(任一 NaN → 聚合 NaN) | + +> 不可算的指标返回 `NaN` 并在 `coverage` 标记 `False`,**不伪造 0/100 分值**(组织规则 #9)。 + +## 3. 为什么这 4 个能算 + +它们都是执行器正常运行的副产物,已存在于 orchestrator 状态: +- `completion` ← 队列本就跟踪任务状态。 +- `collaboration` ← 移交事件、依赖、被分配 Agent 都是派发所需。 +- `cost` ← 计费归因本就记录每任务 `model_cost_usd`;预算在请求里。 +- `robustness` ← 重试逻辑本就维护 `retry_count` 与状态。 + +## 4. 为什么这 5 个算不出(逐项根因) + +| 指标 | 根因(缺什么) | 类别 | 关闭成本 | +|---|---|---|---| +| `gain`(`G_E = Q_swarm − Q_base`) | **本质是对比指标**,单次 swarm run 无法自算;缺 baseline 运行器(Single/Strong/Chain/Sub-Agent)、对比 harness 与数据集 | 缺**基线** | 高(跨团队/基础设施/设计决策) | +| `communication`(`Successful/Total messages`) | peer/WS 消息只**路由**、从不**计数**;无成功/总数计数器,数据流过但未插桩 | 缺**计数器** | 低(加计数器) | +| `governance`(`Compliant/Total ops`) | 有审批机制,但无「受治理/敏感操作 vs 合规」计数;更广的治理面(tool/MCP 权限、allowed_paths 强制)未实现,**没有受治理操作记录可计数**;无审批的 run → 0 操作 → 空集 → NaN | 缺**计数器 + 策略强制点** | 中(计数器 + 部分强制点) | +| `p_decision`(`τ^α·η^β·100`) | v2.0 已给公式,但派发仍是**确定性贪心能力匹配**(`can_agent_run_task`);ACO 决策模型**未实现**——无信息素 `τ`(历史有效性追踪)、无启发式 `η` 评分;没有可测的概率决策 | 缺**整套机制** | 高(新建决策引擎 + 历史库) | +| `reward`(`w₁·S_task + … − w₈·P_rework`) | v2.0 **已给定 `w₁..w₈`**;但**输入**未采集(`Q_quality` 需 TestPass/CodeReview/UserAcceptance、`P_risk`、`P_rework`)→ 仍不可算 | 缺**输入**(权重已定) | 中(质量/CI/风险/返工接入) | + +## 5. 关闭路径(按成本排序) + +1. **低成本(本仓可做)**:`s_communication`、`s_governance` — 在 peer/WS 发送处与受治理操作处加计数器,按 `swarm_id` 聚合。可把真实可算项从 4 提到 6。 +2. **中成本**:`reward` — 权重 v2.0 已定;接入 `Q_quality`(CI/评审/验收)与 risk/rework 输入即可算。 +3. **高成本**: + - `p_decision` — 设计并实现 `τ/η/P` 决策引擎与历史有效性追踪(改变派发机制)。 + - `gain`(及由其驱动的 `Benchmark_Agent`、`G_E,c`、「Swarm > baselines」验收)— 实现 4 类基线运行器 + 统一数据集 + 对比/显著性,属跨团队与基础设施工作(见 baseline-comparison)。 + +## 6. 影响 + +- 在 `gain` 落地前,**无法输出完整 `Benchmark_Agent`,也无法证明 Swarm 优于任一基线** —— 这正是工单「当前更接近多 Agent Workflow Demo」的判断依据。 +- `NaN + coverage=False` 是这一缺口的**诚实证据**;不得以占位分值对外宣称已具备量化自证能力。 diff --git a/docs/benchmark/OWNER-NOTE-cost-normalized-gain.md b/docs/benchmark/OWNER-NOTE-cost-normalized-gain.md new file mode 100644 index 0000000..bab4145 --- /dev/null +++ b/docs/benchmark/OWNER-NOTE-cost-normalized-gain.md @@ -0,0 +1,53 @@ +# Owner Note:`G_E,c` 成本归一化增益公式退化 —— ✅ 已裁定(对称 C_base) + +> 提交:Swarm 仓(HeiCode-Swarm)· 面向:Benchmark Owner(Agent 蜂群指标量化与标准 v2.0 §6.2) +> 状态:**✅ 已裁定(2026-06-09)**。Benchmark Owner 采纳「对称 `C_base`」修正:`C_base` 用基线自身 +> `N_agent` 与 `CostEfficiency` 同式计算。已设为 `benchmark/baselines.compare` 的**默认**(`symmetric_cost=True`); +> 字面 v2.0(`C_base=1.0`)仅作参考(`symmetric_cost=False`)。下文保留问题背景与依据。 + +## 1. 问题 + +v2.0 §6.2: +``` +G_E,c = (Q_swarm / C_swarm) − (Q_base / C_base) +C_swarm = N_agent × (CostEfficiency/100 + 0.5) +C_base = 1.0 +``` +`C_base` 固定为 `1.0`,而 `C_swarm ≈ O(N_agent)`(数量级 ~数倍)。两项尺度不一致: +- `Q_base / C_base = Q_base`(满量级,如 65.5) +- `Q_swarm / C_swarm`(被压缩,如 85.5/12.5 ≈ 6.84) + +→ 基线项恒压倒实验项,**几乎任何多 Agent 蜂群的 `G_E,c` 都为负**,与质量无关。该指标**退化**,无法区分「高性价比蜂群」与「堆 Agent 蜂群」——恰与其设计意图相反。 + +**复现**(`scripts/test-baseline-comparison.py`):Q_swarm=85.5 > Q_sub=65.5(`G_E=+20`),但字面公式 `G_E,c = 6.84 − 65.5 = −58.7`。 + +## 2. 根因 + +`C_base = 1.0` 疑为简化/笔误:它让基线「零成本」,使差值失去「单位成本质量」的可比性。 + +## 3. 建议修复(推荐:对称成本归一化) + +让 `C_base` 与 `C_swarm` 同式计算(用基线自身的 `N_agent` 与 `CostEfficiency`): +``` +C_base = N_agent_base × (CostEfficiency_base/100 + 0.5) +``` +则两项均为「单位成本质量」,可比、非退化,且保留 v2.0「差值 + 动态成本」的设计意图。 + +- 复现示例(对称):`C_base = 3×(166.7/100+0.5) = 6.5` → `G_E,c = 6.84 − 10.08 = −3.24`。这是**有意义**的结果:蜂群质量更高,但用 5 个 Agent 的代价不成比例;当质量增益足够大时 `G_E,c` 可转正。 + +备选:恢复 v1 的**比值形式** `G_E,c = (Q_swarm/C_swarm)/(Q_base/C_base)`(阈值 `>1`),天然尺度无关。 + +## 4. 本仓现状(已就绪,等裁定) + +- `benchmark/metrics.py:cost_normalized_gain` 接受 `c_base` 参数。 +- `benchmark/baselines/compare(..., symmetric_cost=False)`:**默认 = 字面 v2.0(C_base=1.0)**;`symmetric_cost=True` = 上述对称修复。 +- 由 `scripts/test-baseline-comparison.py` 覆盖两种模式。 +- 一旦 Owner 确认,将对称模式设为默认并更新 v2.0 文档与本仓默认值。 + +## 5. 裁定结果(2026-06-09) + +- [x] **采用对称 `C_base`**(默认 `symmetric_cost=True`)。 +- [ ] ~~改回比值形式(阈值 >1)~~ 未采用。 +- [ ] ~~维持 `C_base=1.0`~~ 仅作参考保留(`symmetric_cost=False`)。 +- [x] `CostEfficiency` 口径:统一为 `100·Budget/ActualCost`(= `S_cost` = `E_cost`,Owner 裁定 v2.1)。 +- [ ] **参考系数 ×0.75/0.85/0.90/0.95**:Owner 决定**暂留为元数据、暂不接入公式**,**标记待后续实测量化**(如 `系数 = Q_baseline/Q_reference`)。其含义见 `baseline-comparison.md §1.1`。 diff --git a/docs/benchmark/baseline-comparison.md b/docs/benchmark/baseline-comparison.md new file mode 100644 index 0000000..b755403 --- /dev/null +++ b/docs/benchmark/baseline-comparison.md @@ -0,0 +1,67 @@ +# Baseline Comparison(基线对比) + +> 状态:**规划中(方法已定义,基线运行器未落地)**。 +> +> 依据:**Agent 蜂群指标量化与标准 v2.0 §9.1**。配套:[`emergence-evaluation.md`](./emergence-evaluation.md)、[`cost-normalized-gain.md`](./cost-normalized-gain.md)、[`swarm-benchmark-protocol.md`](./swarm-benchmark-protocol.md)。 + +## 1. 基线与实验组(标准 §9.1) + +| 组 | 系统 | 说明 | 参考系数 | +|---|---|---|---| +| Baseline A | Single Agent | 单 Agent 直接完成(最小基准) | ×0.75 | +| Baseline B | Chain Agent | 串行链式编排(无蜂群协作/评审) | ×0.85 | +| Baseline C | Sub-Agent | 主从结构委派(无对等协作/评审循环) | ×0.90 | +| Baseline D | Strong Agent | 高能力单体(更大模型/更长上下文) | ×0.95 | +| Experimental | **Swarm Agent**(本仓) | 分解→派发→协作→评审/重做→汇总 | — | + +### 1.1 参考系数的含义(暂定值 · 待量化 · 暂不接入公式) + +> ⚠️ **状态:暂定 / 仅元数据**。×0.75/0.85/0.90/0.95 为人为设定的强度先验,标准**未给计算方法**;**不参与任何公式**(仅在 `compare(...)` 结果中作 `base_coefficient` 上报)。**待后续实测量化**(如 `系数 = Q_baseline/Q_reference`,按场景标定)后再决定是否接入。 + +参考系数(Single 0.75 < Chain 0.85 < Sub 0.90 < Strong 0.95)表示**各基线的相对能力强度**——把四类基线按「越接近满能力」排序:Single 最弱(约 0.75),Strong 最强(约 0.95)。其用意是: + +- **打败越强的基线越有价值**。对 Strong(0.95)取得的质量增益,比对 Single(0.75)取得同样增益更能证明涌现——因为强基线的「上行空间」更小(headroom ≈ `1 − 系数`)。 +- 因此它本质是一个**难度 / 可信度因子**:用于在跨基线汇总时给「战胜强基线」的增益更高权重,防止「我们赢了单 Agent」被当作强涌现证据。 + +可能的数学接入方式(**标准 v2.x 未指定,待裁定**): +- 难度加权增益:`weighted_G_E = (Q_swarm − Q_base) × 系数`(战胜强基线权重更高);或 +- headroom 归一化:`G_E_norm = (Q_swarm − Q_base) / (1 − 系数)`(放大对强基线的增益);或 +- 期望基线缩放:`Q_base_expected = 系数 × Q_reference`。 + +**当前实现**:系数仅作为 `compare(...)` 结果里的 `base_coefficient` **元数据上报,不参与任何公式**(`G_E`/`G_E,c` 用原始 `Q_base`)。代码常量见 `benchmark/metrics.py:BASE_COEFFICIENTS`。 + +## 2. 统一采集指标(标准 §10) + +所有组在**同一任务集**上运行,统一采集:`Completion`、`Quality`、`Cost`、`Time`、`Robustness`。 +由此得到每组 `Q_*`(质量,口径见 emergence-evaluation §3)与 `C_*`(成本,见 cost-normalized-gain §3)。 + +## 3. 对比方法 + +1. **同一任务集 / 同一场景**(Coding / Refactoring / Architecture / DevOps / Bug Fix)。 +2. **同等约束**:相同模型网关、相同预算上限口径(避免实验组单独放宽)。 +3. 每组多次运行,报告均值与方差。 +4. 输出: + - `G_E = Q_swarm − Q_base`(对 A/B/C/D 各算一次)。 + - `G_E,c = (Q_swarm/C_swarm)/(Q_base/C_base)`。 + - 成立判定:对**全部** A/B/C/D 满足 `G_E > 0` 且 `G_E,c > 1`。 + +## 4. 公平性约束(防止虚高) + +- 实验组不得使用基线没有的额外资源/预算/工具(除「蜂群协作与评审」本身)。 +- 成本必须计入**多 Agent + 评审重做**的累计(天然反映在按 `swarm_id` 聚合的 `usage`,见 cost-normalized-gain §3)。 +- 报告需同时给出 `G_E` 与 `G_E,c`;只报 `G_E` 不足以验收。 + +## 5. 实现状态 + +| 组件 | 位置 | 状态 | +|---|---|---| +| 基线运行器 A–D | `benchmark/baselines/` | 🔴 未实现 | +| 统一任务集 / 数据集 | `test-data/` + `benchmark/baselines/` | 🔴 未实现 | +| 指标采集 | `benchmark/collectors/` | 🔴 未实现 | +| 对比与显著性 | `benchmark/` | 🔴 未实现 | + +## 6. 待对齐 + +- 四类基线的标准实现边界(尤其 Strong / Chain / Sub-Agent)。 +- 统一任务集与场景数据集。 +- 运行次数、方差/显著性门槛与公平性约束的强制方式。 diff --git a/docs/benchmark/baseline-record-schema.md b/docs/benchmark/baseline-record-schema.md new file mode 100644 index 0000000..8fc31e8 --- /dev/null +++ b/docs/benchmark/baseline-record-schema.md @@ -0,0 +1,71 @@ +# Baseline Run Record Schema(基线运行记录 · 共享契约) + +> 状态:**Schema + 比较评估器已落地;记录生产(尤其 Quality)未落地**。 +> +> 依据:**Agent 蜂群指标量化与标准 v2.0 §6, §9**。实现:`benchmark/baselines/comparison.py`(`BenchmarkRunRecord` / `compare` / `evaluate`)。配套:[`baseline-comparison.md`](./baseline-comparison.md)、[`emergence-evaluation.md`](./emergence-evaluation.md)、[`cost-normalized-gain.md`](./cost-normalized-gain.md)、[`IMPORTANT-metric-coverage-gaps.md`](./IMPORTANT-metric-coverage-gaps.md)。 + +## 1. 目的 + +蜂群验收的核心是 **swarm vs baseline 对比**(尤其 sub-agent,Baseline C ×0.90)。要算 `G_E`/`G_E,c`,**swarm 与每个基线必须在同一任务集、同等约束下,产出同一份记录**。本文定义这份共享记录 `BenchmarkRunRecord`:**两侧程序各自产出**,由本仓评估器统一比较。 + +## 2. 共享记录 `BenchmarkRunRecord` + +| 字段 | 类型 | 含义 | +|---|---|---| +| `system` | str | `swarm` / `single` / `chain` / `sub` / `strong` | +| `scenario` | str | `coding` / `refactoring` / `architecture` / `devops` / `bugfix` | +| `task_set_id` | str | 共享任务集 ID(**同一 id 才可比**) | +| `n_agent` | int | 该系统使用的 Agent 数 | +| `completed_tasks` / `total_tasks` | int | 完成度(Completion) | +| `test_pass_rate` / `code_review_score` / `user_acceptance` | float(0–100) | 质量三输入(Quality) | +| `budget_usd` / `actual_cost_usd` | float | 计划/实测成本(Cost、CostEfficiency) | +| `model_tokens` | int | token 用量 | +| `target_time_s` / `actual_time_s` | float | 计划/实测耗时(Time、Speed) | +| `recovered_failures` / `total_failures` | int | 恢复/总失败(Robustness) | + +JSON 示例: +```json +{ + "system": "sub", "scenario": "coding", "task_set_id": "coding-set-1", "n_agent": 3, + "completed_tasks": 9, "total_tasks": 10, + "test_pass_rate": 70, "code_review_score": 60, "user_acceptance": 65, + "budget_usd": 10, "actual_cost_usd": 6, "model_tokens": 42000, + "target_time_s": 600, "actual_time_s": 720, "recovered_failures": 1, "total_failures": 2 +} +``` + +## 3. 派生量(评估器,已实现) + +- `Q = quality(rec)` = `0.4·TestPass + 0.3·CodeReview + 0.3·UserAcceptance` +- `CostEfficiency = 100·Budget/ActualCost` +- 统一采集 `unified_metrics(rec)`:Completion / Quality / CostEfficiency / Speed / Robustness(v2.0 §9) +- `compare(swarm_rec, base_rec)`(默认 `symmetric_cost=True`,Owner 修正 2026-06-09): + - `G_E = Q_swarm − Q_base` + - `C_swarm = N_agent_swarm·(CostEfficiency_swarm/100 + 0.5)`;`C_base = N_agent_base·(CostEfficiency_base/100 + 0.5)`(对称) + - `G_E,c = (Q_swarm/C_swarm) − (Q_base/C_base)` + - 字面 v2.0(`C_base=1.0`)仅 `symmetric_cost=False` 保留参考;背景见 [`OWNER-NOTE-cost-normalized-gain.md`](./OWNER-NOTE-cost-normalized-gain.md)。 +- `evaluate(swarm_rec, [baselines])`:对全部基线比较;`swarm_valid` 要求对每个基线 `G_E>0` 且 `G_E,c>0`。 + +## 4. 谁产出什么 + +| 字段族 | swarm 侧现状 | sub-agent 侧需产出 | +|---|---|---| +| Completion (`completed/total`) | ✅ 任务状态 | ✅ 需提供 | +| Cost (`actual_cost_usd`/`model_tokens`)、`n_agent` | ✅ usage 已采集 | ✅ 需提供 | +| Time (`actual_time_s`) | ✅ 时间戳 | ✅ 需提供 | +| Robustness (`recovered/total_failures`) | 🟡 需计数 | ✅ 需提供 | +| **Quality (`test_pass_rate`/`code_review_score`/`user_acceptance`)** | 🔴 **未插桩** | 🔴 **需插桩** | + +> **关键阻塞**:Quality 在 **swarm 与 sub-agent 两侧都未插桩**(无 CI/测试通过率、评审分、验收)。没有 Quality 就无 `Q` → 无 `G_E`。这是「记录已可比较、但记录尚不可生产」的根本原因。 + +## 5. 接入路径 + +1. 双方各实现一个「跑统一任务集 → 产出 `BenchmarkRunRecord`」的 runner(sub-agent 侧在其程序内;swarm 侧可由 `SwarmRunMetricsCollector` 扩展 + Quality 插桩)。 +2. 统一任务集与 `task_set_id`、同等约束(同模型网关、同预算口径)。 +3. 把两侧记录交给 `benchmark/baselines/evaluate(...)` → 得 `G_E`/`G_E,c` 与 `swarm_valid`。 + +## 6. 待对齐 + +- Quality 三输入的采集口径与 CI/评审/验收来源。 +- `CostEfficiency` 口径(`S_cost` vs `E_cost`)与基线参考系数(×0.75/0.85/0.90/0.95)在公式中的确切作用。 +- 统一任务集与数据集(各场景)。 diff --git a/docs/benchmark/cost-normalized-gain.md b/docs/benchmark/cost-normalized-gain.md new file mode 100644 index 0000000..7083008 --- /dev/null +++ b/docs/benchmark/cost-normalized-gain.md @@ -0,0 +1,63 @@ +# Cost-Normalized Gain(成本归一化增益) + +> 状态:**规划中(公式已对齐,ROI 评估器未落地)**。 +> +> 依据:**Agent 蜂群指标量化与标准 v2.0 §6.2**。配套:[`emergence-evaluation.md`](./emergence-evaluation.md)、[`swarm-metrics-schema.md`](./swarm-metrics-schema.md)、[`usage-billing` 用量](../integration/usage-billing-schema.md)。实现:`benchmark/metrics.py` 的 `cost_normalized_gain` / `swarm_cost`。 + +## 1. 公式(v2.0 + Owner 修正 2026-06-09:对称成本归一化) + +``` +G_E,c = (Q_swarm / C_swarm) − (Q_base / C_base) + +C_swarm = N_agent_swarm × (CostEfficiency_swarm / 100 + 0.5) +C_base = N_agent_base × (CostEfficiency_base / 100 + 0.5) +``` + +- `Q_*`:质量(同 emergence-evaluation §3)。`CostEfficiency`:成本效率 `100·Budget/ActualCost`(**= §5.1 `S_cost` = §4.1 `E_cost`,同一量**,Owner 裁定 v2.1)。 +- 判读:`G_E,c > 0` 表示**单位成本质量**优于基线;`< 0` 表明蜂群以更高的 Agent 成本换取质量、性价比不及基线,应缩减 `N_agent`。 + +> ✅ **Owner 修正(2026-06-09)**:原始 v2.0 §6.2 取 `C_base = 1.0`,与 `C_swarm ≈ O(N_agent)` 尺度不一致, +> 致使几乎任何多 Agent 蜂群 `G_E,c < 0`(与质量无关,指标退化)。经 Benchmark Owner 裁定,改为**对称归一化** +> (`C_base` 用基线自身 `N_agent` 与 `CostEfficiency` 同式计算),使两项均为单位成本质量、可比。 +> 实现为 `benchmark/baselines.compare` 的**默认**(`symmetric_cost=True`);字面 v2.0(`C_base=1.0`)仅 `symmetric_cost=False` 保留参考。背景见 [`OWNER-NOTE-cost-normalized-gain.md`](./OWNER-NOTE-cost-normalized-gain.md)。 + +## 2. 目的 + +防止通过**无限扩张 Agent 数量 / Token** 获得虚假涌现增益:`C_swarm` 随 `N_agent` 线性增长,堆 Agent 会拉低 `Q_swarm/C_swarm`。蜂群验收**不只**看 `G_E > 0`,还要 `G_E,c > 0`。 + +## 3. 成本 C 口径 + +`C` 建议为单次任务集的总成本,至少包含(数据源见 usage-billing-schema): +``` +C = model_cost_usd(所有 task / 所有 review 重做累计) + [+ runtime / 基础设施成本,待接入] +``` + +| 成本项 | 本仓可采集 | +|---|---| +| `model_cost_usd`(含多 Agent、多 review 重做累计) | 🟡 usage 已采集,按 `swarm_id` 聚合即可 | +| `model_tokens`(token 效率视角) | 🟡 已采集 | +| 基础设施成本(cpu/memory/Pod) | 🔴 未接入账本(见 usage-billing §6) | + +> 多 Agent / review 重做的成本**已隐含累计**在按 `swarm_id` 聚合的 `usage` 中,因此 `C_swarm` 天然反映「堆 Agent / 重做」的代价——这正是 `G_E,c` 要惩罚的。 + +## 4. 衍生视角(Leaderboard) + +- **Token 效率**:`Q / total_tokens`。 +- **Cost Efficiency**:`Q / model_cost_usd`。 +- **Swarm ROI**:`G_E,c` 本身。 + +## 5. 实现状态 + +| 组件 | 状态 | +|---|---| +| `usage` 成本聚合(按 swarm_id) | 🟡 数据具备,无聚合器 | +| 成本归一化评估器(G_E,c) | 🔴 未实现 | +| 多 Agent ROI / token 效率 / 成本效率 | 🔴 未实现 | +| 基础设施成本纳入 C | 🔴 未接入 | + +## 6. 待对齐 + +- `C` 是否纳入基础设施成本(与 Billing Team / usage-billing §7 对齐)。 +- `Q` 口径与 emergence-evaluation 一致化。 +- `CostEfficiency` 的确切口径(`S_cost` vs `E_cost`)与 `G_E,c` 的验收阈值(`>0` 之外是否设场景化下限)。 diff --git a/docs/benchmark/emergence-evaluation.md b/docs/benchmark/emergence-evaluation.md new file mode 100644 index 0000000..6b94f5b --- /dev/null +++ b/docs/benchmark/emergence-evaluation.md @@ -0,0 +1,68 @@ +# Emergence Evaluation(涌现增益评估) + +> 状态:**规划中(公式已对齐,基线运行器/评估器未落地)**。 +> +> 依据:**Agent 蜂群指标量化与标准 v2.0 §6.1, §9**。配套:[`baseline-comparison.md`](./baseline-comparison.md)、[`cost-normalized-gain.md`](./cost-normalized-gain.md)、[`swarm-benchmark-protocol.md`](./swarm-benchmark-protocol.md)。 + +## 1. 原始增益 + +``` +G_E = Q_swarm − Q_base +``` + +`Q_base` 取自四类基线之一(标准 §6.1,附**参考系数**,由低到高): + +| 基准 | 类型 | 参考系数 | +|---|---|---| +| Baseline A | Single Agent | ×0.75 | +| Baseline B | Chain Agent | ×0.85 | +| Baseline C | Sub-Agent | ×0.90 | +| Baseline D | Strong Agent | ×0.95 | + +> ⚠️ **参考系数为暂定值(待量化)**:×0.75/0.85/0.90/0.95 是人为设定的强度先验,标准未给计算方法;**当前不参与公式**(`G_E` 用原始 `Q_base`),仅作元数据。后续应实测量化(见 [`baseline-comparison.md §1.1`](./baseline-comparison.md))。代码常量见 `benchmark/metrics.py:BASE_COEFFICIENTS`。 + +## 2. 成立条件(蜂群是否真正优于基线) + +蜂群成立要求对**全部**基线为正增益: +``` +Q_swarm > Q_single ∧ Q_swarm > Q_strong ∧ Q_swarm > Q_chain ∧ Q_swarm > Q_sub +``` +否则蜂群不成立(仅是「多 Agent 工作流」而非有涌现的蜂群)。 + +> 仅有原始增益不足以验收:还须经成本归一化(见 [`cost-normalized-gain.md`](./cost-normalized-gain.md)),证明增益非「堆 Agent / Token」虚高。 + +## 3. Q(质量)口径 + +`Q` 采用统一质量度量。建议复用标准 §5.3: +``` +Q = Q_quality = 0.4·TestPassRate + 0.3·CodeReviewScore + 0.3·UserAcceptance +``` +并在每组统一采集 `Completion / Quality / Cost / Time / Robustness`(标准 §10),按场景(Coding / Refactoring / Architecture / DevOps / Bug Fix)分别计算 `G_E`。 + +> ⚠️ `Q` 的精确口径(是否等于 `Q_quality`,或综合 Completion/Robustness)v2.0 未唯一指定 → 待对齐。 + +## 4. 评测流程(草案) + +1. 固定任务集(按场景)。 +2. 在 A/B/C/D 与 Swarm 上**同一任务集**运行,统一采集指标(见 swarm-metrics-schema §0)。 +3. 计算每场景 `Q_*`,得 `G_E = Q_swarm − Q_base`。 +4. 进入成本归一化(`G_E,c`)。 +5. 多次运行取均值并报告方差/显著性。 + +## 5. 实现状态 + +| 组件 | 位置(规划) | 状态 | +|---|---|---| +| 基线运行器 A–D | `benchmark/baselines/` | 🔴 未实现 | +| 基线 benchmark 套件 / 数据集 | `benchmark/baselines/` + `test-data/` | 🔴 未实现 | +| 基线对比 | baseline-comparison | 🔴 未实现 | +| 涌现评估器(G_E) | `benchmark/` | 🔴 未实现 | +| 归一化增益评估器(G_E,c) | cost-normalized-gain | 🔴 未实现 | + +> 在以上落地前,**不得宣称已验证蜂群涌现能力**(重大能力缺口)。 + +## 6. 待对齐 + +- `Q` 的唯一口径。 +- 四类基线的标准实现(尤其 Strong Agent / Chain Agent / Sub-Agent 的定义边界)与统一数据集。 +- 运行次数、方差/显著性门槛。 diff --git a/docs/benchmark/governance-score.md b/docs/benchmark/governance-score.md new file mode 100644 index 0000000..492c80f --- /dev/null +++ b/docs/benchmark/governance-score.md @@ -0,0 +1,53 @@ +# Governance Score(治理评分) + +> 状态:**规划中(公式已对齐,治理计数器未落地)**。 +> +> 依据:**Agent 蜂群指标量化与标准 v2.0 §4.1, §5.1**(公式未变)。配套:[`security-boundary`](../integration/security-boundary.md)、[`audit-trace`](../integration/audit-trace-schema.md)、[`swarm-metrics-schema.md`](./swarm-metrics-schema.md)。 + +## 1. 公式 + +``` +G_gov = CompliantActions / SensitiveActions × 100 # 执行层(奖励 R 中 w₆=0.10) +S_governance = CompliantOperations / TotalOperations × 100 # 蜂群层(S_swarm 中 0.10) +``` + +`Gov`(综合治理能力,进入 `Benchmark_Agent`)由上述构成。 + +## 2. 何为「需治理 / 合规」操作 + +需纳入治理计数的操作类别(对齐 security-boundary): +- **审批门**:高危操作是否走客户端审批(`approval.requested` → 审批回执)。 +- **权限**:资源访问是否在 `allowed_actions`/`constraints` 范围内。 +- **密钥**:是否仅用 `secret_ref`、无明文泄露。 +- **MCP / 工具边界**:工具调用是否在允许集合内(`sk_tool.*`)。 +- **敏感操作**:生产部署、DB 写、外部 API 等是否满足策略与 TTL。 +- **审计**:是否产生可追溯记录(见 audit-trace)。 + +「合规」= 上述操作满足策略;`S_governance` = 合规操作数 / 总(敏感/受治理)操作数。 + +## 3. 本仓可采集状态 + +| 治理维度 | 现状 | +|---|---| +| 审批门(waiting_approval + approvals 回执) | 🟡 状态机已实现,但未计数「合规/总」 | +| 密钥(`azkv://` 强校验、明文拒绝、脱敏) | 🟡 已强制,但未作为治理计数项 | +| 权限(allowed_actions/constraints 运行时强制) | 🔴 未运行时强制(见 security-boundary §3) | +| MCP / 工具边界 | 🔴 未实现(无工具权限引擎) | +| 敏感操作策略 + TTL 校验 | 🔴 未实现(审批逐项校验待加强) | +| 审计可追溯 | 🟡 事件流可追溯,未量化为治理分 | + +> 结论:治理当前仍**偏 README/机制级**,缺少 `CompliantOperations / TotalOperations` 的统一计数与导出 → `S_governance` / `Gov` 暂不可量化输出。 + +## 4. 实现状态 + +| 组件 | 状态 | +|---|---| +| 治理操作计数器(governed/compliant/total) | 🔴 未实现 | +| 治理评分导出(S_governance / Gov) | 🔴 未实现 | +| 与 audit 事件的绑定 | 🔴 未实现 | + +## 5. 待对齐 + +- 「敏感 / 受治理操作」的精确清单与判定(Security/Governance Team)。 +- 「合规」判定规则(策略命中、审批命中、范围内、TTL 内)。 +- 计数与导出口径(按 `swarm_id` 聚合,进入 `SwarmMetrics.governance`)。 diff --git a/docs/benchmark/swarm-benchmark-protocol.md b/docs/benchmark/swarm-benchmark-protocol.md new file mode 100644 index 0000000..c137545 --- /dev/null +++ b/docs/benchmark/swarm-benchmark-protocol.md @@ -0,0 +1,88 @@ +# Swarm Benchmark Protocol + +> 状态:**规划中(协议已成文,采集器/基线/回放未落地)**。 +> +> 依据:**Agent 蜂群指标量化与标准 v2.0**。本目录其余文档对各分量展开:[`swarm-metrics-schema.md`](./swarm-metrics-schema.md)、[`emergence-evaluation.md`](./emergence-evaluation.md)、[`baseline-comparison.md`](./baseline-comparison.md)、[`cost-normalized-gain.md`](./cost-normalized-gain.md)、[`governance-score.md`](./governance-score.md)、[`telemetry-architecture.md`](./telemetry-architecture.md)。 + +## 1. 目标 + +统一蜂群评测体系:支持不同 Agent Framework 横向比较、持续优化与回归测试、Benchmark 排行榜。**最终验收不是「能运行」或「能协作」,而是通过本协议自证**(尤其涌现增益 `G_E` 与成本归一化增益 `G_E,c`)。 + +## 2. 综合评分 Benchmark_Agent + +``` +Benchmark_Agent = λ1·S_swarm + λ2·G_E + λ3·R + λ4·O + λ5·Gov +``` + +| 分量 | 含义 | 展开文档 | +|---|---|---| +| `S_swarm` | 蜂群能力 | swarm-metrics-schema §蜂群层 | +| `G_E` | 涌现增益(vs baseline) | emergence-evaluation | +| `R` | 奖励函数 | swarm-metrics-schema §执行层 | +| `O` | 可观测性 | telemetry-architecture | +| `Gov` | 治理能力 | governance-score | + +**λ 权重(v2.0,强制 `Σλᵢ = 1.0`)**:`λ1=0.30`(S_swarm)· `λ2=0.20`(G_E)· `λ3=0.25`(R)· `λ4=0.15`(O)· `λ5=0.10`(Gov)。`benchmark/metrics.py:benchmark_agent` 会校验 `Σλ=1.0`。 + +`S_swarm` 子权重(v2.0,未变): +``` +S_swarm = 0.25·S_completion + 0.20·S_gain + 0.15·S_collaboration + + 0.10·S_communication + 0.10·S_cost + 0.10·S_robustness + 0.10·S_governance +``` + +## 3. Benchmark Protocol(基线与实验组) + +| 组 | 系统 | +|---|---| +| Baseline A | Single Agent | +| Baseline B | Chain Agent | +| Baseline C | Sub-Agent | +| Baseline D | Strong Agent | +| Experimental | **Swarm Agent**(本仓) | + +所有组**统一采集**:`Completion`、`Quality`、`Cost`、`Time`、`Robustness`。 +蜂群成立的硬条件:`Swarm > Single`、`Swarm > Chain`、`Swarm > Sub-Agent`、`Swarm > Strong`(见 emergence-evaluation),且经成本归一化后增益非「堆 Agent/Token」虚高(见 cost-normalized-gain)。 + +## 3.1 超参数向量 Θ(v2.0 §7.2) + +``` +Θ = [α, β, ρ, N_agent, ε] Θ* = argmax(Benchmark_Agent) +``` +推荐初值:`α=1.0`、`β=2.0`、`ρ=0.10`(信息素挥发率)、`N_agent=5`、`ε=0.10`(探索率)。见 `benchmark/metrics.py:THETA_DEFAULTS`。 + +## 4. 场景(Leaderboard 维度) + +Coding · Refactoring · Architecture · DevOps · Bug Fix。 + +## 5. Leaderboard 展示字段 + +`Benchmark_Agent`、`S_swarm`、`G_E`、`G_E,c`、`Reward`、`Cost Efficiency`。 + +## 6. 实现状态(本仓) + +| 组件 | 位置(规划) | 状态 | +|---|---|---| +| 指标定义 / schema | `docs/benchmark/*`、`SwarmMetrics` | ✅ 已成文 | +| 采集器 `SwarmMetricsCollector` | `benchmark/collectors/` | 🟡 部分落地(`SwarmRunMetricsCollector` 真实计算 completion/collaboration/cost/robustness;其余 NaN+coverage) | +| 基线运行器 A–D | `benchmark/baselines/` | 🔴 未落地 | +| 回放 replay | `benchmark/replay/` | 🔴 未落地 | +| 排行榜 leaderboard | `benchmark/leaderboard/` | 🔴 未落地 | +| 数据采集架构 | telemetry-architecture | 🔴 未接入(OTel/Prometheus/ClickHouse/ES) | + +> 在采集器与基线运行器落地前,**不得宣称本仓已具备 Agent Swarm 量化自证能力**。 + +## 7. 验收标准(对齐工单) + +- [ ] 能输出完整 `Benchmark_Agent`。 +- [ ] 能量化 `S_swarm`、`G_E`、`G_E,c`、`Gov`。 +- [ ] 能回放一次 benchmark execution。 +- [ ] 能证明 `Swarm > Single / Chain / Sub-Agent`。 +- [ ] 能证明增益不依赖无限 token/GPU 堆叠(成本归一化)。 + +## 8. 待对齐 + +- ~~`λ1…λ5` 数值权重~~ ✅ v2.0 已给定(Σλ=1.0)。 +- `Θ*` 调优口径与搜索方法。 +- 各基线(Single/Chain/Sub-Agent/Strong)的标准实现与数据集。 +- `Quality` 的统一口径(TestPassRate / CodeReviewScore / UserAcceptance 来源)。 +- 数据源与平台(telemetry-architecture)。 diff --git a/docs/benchmark/swarm-metrics-schema.md b/docs/benchmark/swarm-metrics-schema.md new file mode 100644 index 0000000..5ab72a9 --- /dev/null +++ b/docs/benchmark/swarm-metrics-schema.md @@ -0,0 +1,103 @@ +# Swarm Metrics Schema(v2.0) + +> 状态:**公式已对齐标准 v2.0;采集器部分落地**。 +> +> 依据:**Agent 蜂群指标量化与标准 v2.0**(§3–§8)。配套:[`swarm-benchmark-protocol.md`](./swarm-benchmark-protocol.md)、[`metric-coverage-gaps.md`](./metric-coverage-gaps.md)、[`telemetry-architecture.md`](./telemetry-architecture.md)。 +> +> 实现:公式见 `benchmark/metrics.py`(纯函数,含 v2.0 推荐权重常量 `TAU_WEIGHTS`/`ETA_WEIGHTS`/`REWARD_WEIGHTS`/`SWARM_WEIGHTS`/`LAMBDA_WEIGHTS`/`THETA_DEFAULTS`);真实采集见 `benchmark/collectors/run_collector.py`(`SwarmRunMetricsCollector`)。 + +## 0. 采集接口(标准 §8.1) + +```python +@dataclass +class SwarmMetrics: + tau: float # 信息素得分 + eta: float # 启发式得分 + p_decision: float # 决策概率 + reward: float # 执行奖励 + s_completion: float + s_gain: float + s_collaboration: float + s_communication: float + s_cost: float + s_robustness: float + s_governance: float + s_swarm: float # 蜂群总分 + g_e: float # 涌现增益 + g_e_cost: float # 成本归一化增益 + benchmark: float # 综合评分 +``` +`SwarmRunMetricsCollector` 真实计算 `s_completion`/`s_collaboration`/`s_cost`/`s_robustness`,其余返回 `NaN` 并在 `coverage` 标记 `False`(不伪造分值)。 + +## 1. 决策层(标准 §3) + +``` +τ(s,a,r) = w₁·Success + w₂·Quality + w₃·Acceptance − w₄·Cost − w₅·Time − w₆·Risk − w₇·Rollback +η(s,a,r) = γ₁·Match + γ₂·Urgency + γ₃·Dependency + γ₄·Resource + γ₅·Confidence − γ₆·Risk − γ₇·BudgetPressure +P(s,a,r) = τ^α·η^β / Σ τ_i^α·η_i^β +P_decision = τ^α · η^β · 100 # v2.0 §3.3(变更) +``` + +**τ 权重(v2.0)**:Success 0.25 · Quality 0.20 · **Acceptance 0.20(提升为一级因子)** · Cost 0.10 · Time 0.10 · Risk 0.08 · Rollback 0.07。 +**η 权重(v2.0)**:Match 0.25 · Urgency 0.15 · Dependency 0.15 · Resource 0.15 · **Confidence 0.10(新增)** · Risk 0.10 · BudgetPressure 0.10。 + +| 因子 | 数据来源(标准) | 本仓可采集 | +|---|---|---| +| τ.Success | Task 完成记录 | ✅ | +| τ.Quality | 测试 / CI/CD | 🔴 无 CI 接入 | +| τ.Acceptance | 人工验收日志 | 🟡 评审 accepted | +| τ.Cost / Time | 资源监控 / Runtime 日志 | 🟡 usage / 时间戳 | +| τ.Risk / Rollback | 安全审计 / Git·部署 | 🔴 | +| η.Match / Dependency | 能力画像 / DAG | ✅ | +| η.Confidence | Agent 自评接口 | 🔴 未实现 | +| η.Urgency / Resource / Risk / BudgetPressure | 优先级 / 授权 / 风险引擎 / 预算 | 🔴 / 🟡 / 🔴 / 🟡 | +| P_decision | 上述综合 | 🔴 无 τ/η 引擎 | + +## 2. 执行层(标准 §4) + +``` +R = w₁·S_task + w₂·Q_quality + w₃·V_speed + w₄·E_cost + w₅·R_robust + w₆·G_gov − w₇·P_risk − w₈·P_rework +S_task = CompletedTasks/TotalTasks×100 Q_quality = 0.4·TestPass + 0.3·CodeReview + 0.3·UserAcceptance +V_speed = 100·TargetTime/ActualTime E_cost = 100·ExpectedCost/ActualCost +R_robust = RecoveredFailures/TotalFailures×100 G_gov = CompliantActions/SensitiveActions×100 +P_rework = ReworkCount/TotalTasks×100 +``` + +**R 权重(v2.0,已给定)**:S_task **0.20** · Q_quality **0.20(与完成度对齐)** · V_speed 0.12 · E_cost 0.13 · R_robust 0.13 · G_gov 0.10 · P_risk 0.07 · P_rework 0.05。 + +| 指标 | 本仓可采集 | +|---|---| +| `S_task` | ✅ | +| `Q_quality` | 🔴 需 TestPass/CodeReview/UserAcceptance(无 CI/评分) | +| `V_speed` / `E_cost` | 🟡 Actual 有;Target/Expected 需基线 | +| `R_robust` | 🟡 重试/恢复未计数 | +| `G_gov` / `P_risk` / `P_rework` | 🔴 无敏感操作/风险/返工计数 | + +> v2.0 已给定全部 `w*`/`γ*` 权重,但多数**输入**仍未采集 → `τ`/`η`/`reward`/`p_decision` 暂不可算。 + +## 3. 蜂群层(标准 §5,权重未变) + +``` +S_swarm = 0.25·S_completion + 0.20·S_gain + 0.15·S_collaboration + + 0.10·S_communication + 0.10·S_cost + 0.10·S_robustness + 0.10·S_governance +S_completion = CompletedTasks/TotalTasks×100 +S_collaboration= 0.5·HandoffSuccessRate + 0.3·DependencyResolutionRate + 0.2·WorkloadBalanceScore +S_communication= SuccessfulMessages/TotalMessages×100 +S_cost = 100·Budget/ActualUsage +S_robustness = RecoveredFailures/TotalFailures×100 +S_governance = CompliantOperations/TotalOperations×100 +``` + +| 指标 | 状态 | +|---|---| +| `s_completion` / `s_collaboration` / `s_cost` / `s_robustness` | ✅ 真实可算(采集器) | +| `s_gain` | 🔴 需基线(见 emergence-evaluation) | +| `s_communication` | 🔴 未计数(无消息 telemetry) | +| `s_governance` | 🟡 仅审批可派生 | +| `s_swarm` | 🔴 含 gain/communication NaN → 暂为 NaN | + +## 4. 说明与待对齐 + +- **成本口径统一(v2.1)**:`S_cost`(§3)、`E_cost`(§2)、`CostEfficiency`(见 cost-normalized-gain)为**同一量** `100×Budget/ActualCost`。 +- `α/β/ρ/N_agent/ε` 取值:标准 §7.2 给推荐初值(1.0 / 2.0 / 0.10 / 5 / 0.10),调优口径待定。 +- `Q_quality` 三项来源、`SuccessfulMessages/TotalMessages`、`Recovered/Total`、`Compliant/Total`、`Rework/Total` 的精确计数定义。 diff --git a/docs/benchmark/telemetry-architecture.md b/docs/benchmark/telemetry-architecture.md new file mode 100644 index 0000000..59f40db --- /dev/null +++ b/docs/benchmark/telemetry-architecture.md @@ -0,0 +1,53 @@ +# Telemetry Architecture(数据采集架构) + +> 状态:**规划中(部分信号已发出,统一采集管道未接入)**。 +> +> 依据:**Agent 蜂群指标量化与标准 v2.0 §8.2**。配套:[`swarm-metrics-schema.md`](./swarm-metrics-schema.md)、[`event-schema`](../integration/event-schema.md)、[`audit-trace`](../integration/audit-trace-schema.md)。`O`(可观测性)分量来源于本文。 + +## 1. 数据来源矩阵(标准 §8.2) + +| 指标类型 | 数据来源 | 推荐平台 | 本仓现状 | +|---|---|---|---| +| 任务完成 | Task Logs、Handoff Logs | Elasticsearch | ✅ 任务状态 + 事件流(按 `swarm_id`);未汇入 ES | +| 成本 / Token | Runtime Metrics | Prometheus | 🟡 usage 已采集;Prometheus 指标可抓取 | +| 质量 | CI/CD Results、Code Review | 各 CI 平台 | 🔴 未接入 | +| 治理合规 | Audit Logs | OpenTelemetry | 🟡 事件流可作审计源,未独立留存 | +| 通信 | WebSocket Logs | ClickHouse | 🟡 连接事件有日志;消息成功率未计数 | +| 基础设施 | Infrastructure Metrics | Prometheus | 🔴 未接入(cpu/memory/Pod) | + +## 3. 本仓已发出的信号 + +| 信号 | 实现 | 去向 | +|---|---|---| +| Prometheus 指标 | ✅ 编排器 `GET /metrics`;Agent `METRICS_PORT` | 可被 Prometheus 抓取(`k8s/prometheus-config.yaml`) | +| OpenTelemetry tracing | 🟡 `orchestrator/tracing.py`(`OTEL_EXPORTER_OTLP_ENDPOINT`) | 需配置 collector | +| 事件流(lifecycle/DAG/handoff/usage) | ✅ 按 `swarm_id` 持久化 + 回调 | Redis + HM 回调;可导出 ClickHouse/ES(未接) | +| 任务/用量/预算 | ✅ `/tasks`、`/metrics`、`budget.alert` | REST + 事件 | + +## 4. 目标采集管道(草案) + +``` +Agent / Orchestrator + ├─ Prometheus ← /metrics(指标:任务、时长、并发、被拒/重复、reconnect…) + ├─ OpenTelemetry → OTLP collector → trace 存储(task/agent/handoff span) + └─ 事件流(event-schema) → 导出器 → ClickHouse / Elasticsearch(用于 benchmark 聚合与回放) + ↑ + SwarmMetricsCollector 读取并产出 SwarmMetrics +``` + +## 5. 缺口 + +| 项 | 状态 | +|---|---| +| Prometheus 指标暴露 | ✅ 已有(指标项可再补:消息成功率、恢复率、治理计数) | +| OTel trace 实际导出 | 🟡 模块在,collector/span 覆盖待完善 | +| 事件 → ClickHouse/ES 导出器 | 🔴 未实现 | +| CI/CD、Git、基础设施指标接入 | 🔴 未实现 | +| 统一 `SwarmMetricsCollector` 落地 | 🔴 未实现(见 swarm-metrics-schema §0) | +| 刷新可恢复 / 断线回补(前端) | 🟡 `/logs?cursor=`(见 frontend-event-api) | + +## 6. 待对齐 + +- 选定后端(ClickHouse / ES)与 schema、留存期(与 Audit Team)。 +- 指标命名规范与标签(`swarm_id`/`agent_role`/`scenario`)。 +- 导出器归属(本仓 exporter vs 平台统一采集)。 diff --git a/docs/integration/agent-capability-schema.md b/docs/integration/agent-capability-schema.md new file mode 100644 index 0000000..2e447b8 --- /dev/null +++ b/docs/integration/agent-capability-schema.md @@ -0,0 +1,65 @@ +# Agent 能力与调度 Schema(Capability / Registry / Routing) + +> 状态:**DRAFT / 待对齐 Infra & Scheduling Team**。 +> +> 依据:`heicode-mananger/docs/heicode.md §五`、`docs/integration/heicode-am-contract.md §2`。配套:[`runtime-contract.md`](./runtime-contract.md)、[`security-boundary.md`](./security-boundary.md)。 + +## 1. 现状 + +- 当前能力调度为 **swarm 私有实现**:Agent 以 `AGENT_CAPABILITIES` 注册到编排器(Redis 注册表),编排器按「`required_capabilities ⊆ agent.capabilities` + 剩余容量」派发。 +- **尚未接入统一 agent registry / capability center / quota / tenant / region·GPU 调度 / provider routing / model policy**。这些归 Infra/Scheduling,未在本仓实现(避免 Runtime Scheduling 双实现)。 + +## 2. 已实现的能力模型(本仓) + +### 2.1 Agent 注册(`AgentMetadata`) +```jsonc +{ "agent_id": "...", "status": "idle|busy|handoff-pending|failed", + "capabilities": ["python","testing", ...], "current_task_id": "...|null", + "last_heartbeat": 0.0 } +``` +另:register/heartbeat 上报 `available_slots`(剩余容量),编排器据此避免向满载 Agent 派发。 + +### 2.2 任务侧能力需求 +任务携带 `agent_role` 与 `required_capabilities`;派发用 `can_agent_run_task`(`required ⊆ capabilities`,空需求视为通用可执行)。 + +### 2.3 能力别名(`handoff_logic.CAPABILITY_ALIASES`) +`code_generation` / `python` / `general` 等扩展为本地等价能力集合,用于移交决策与匹配宽松化。 + +### 2.4 角色(role) +- Manager/产品口径角色(`heicode.md §五`):`product`、`frontend`、`backend`、`reviewer`、`ops`。 +- 本仓规划回退默认角色:`implementation`、`testing`、`documentation`(`agent_role` 字段,注入 `specialist_role`)。 + +## 3. 角色 ↔ 资源 ↔ env(对齐 AM) + +资源授权(Resource Grant,Manager 侧)按角色绑定资源,HM 解析为固定 env 名注入 Agent(`heicode-am-contract §2`): + +| 资源类型 | provider | 非密 env | 密钥 env(经 secret_ref) | +|---|---|---|---| +| git | github/gitea/gitlab | `GIT_PROVIDER` `GIT_REPO_URL` `GIT_DEFAULT_BRANCH` | `GIT_TOKEN` | +| database | mysql / postgres | `MYSQL_*` / `POSTGRES_*`(host/port/db/user) | `MYSQL_PASSWORD` / `POSTGRES_PASSWORD` | +| storage | azure blob | `AZURE_BLOB_ACCOUNT_NAME` `AZURE_BLOB_CONTAINER` | `AZURE_BLOB_ACCOUNT_KEY` | + +角色/指令经 `AGENT_ROLE_NAME` + `AGENT_INSTRUCTION_TEXT` 注入(AM 约定)。 + +## 4. 目标统一调度 Schema(待 Infra 定义) + +| 维度 | 字段(建议) | 状态 | +|---|---|---| +| Registry | `agent_id`、`capabilities`、`role`、`framework`、`version`、`health` | 🟡 本仓有私有注册表,未接统一中心 | +| Capability center | 能力字典、能力版本、能力↔工具映射 | 🔴 未接入 | +| Quota | `max_concurrent`、`token_quota`、`cost_quota`(按 user/channelId) | 🟡 仅 Agent 侧 `MAX_CONCURRENT_TASKS`/容量 | +| Tenant isolation | —(按标准用 user/channelId,不引入 tenant) | ⛔ 不在本仓 | +| Region / GPU scheduling | `region`、`gpu_class`、`node_pool` | 🔴 未接入(AKS/Infra) | +| Provider routing | `provider`、`model_policy`、`fallback` | 🔴 未接入(NewAPI/网关侧) | +| Model policy | `model_ref`、`profile`、`budget` | 🟡 Agent 用 `OPENAI_MODEL`;统一 policy 未接 | + +## 5. 缺口 + +- 🔴 统一 agent registry / capability center / provider routing / region·GPU 调度:未实现,归 Infra/Scheduling。 +- 🟡 配额仅 Agent 并发/容量级;无 user/channelId 维度配额中心。 +- ⛔ tenant 维度:按标准不引入。 +- ⚠️ 风险:若 Infra 落地统一调度,需收敛本仓私有派发,避免 Runtime Scheduling 双实现。 + +## 6. 待对齐对象 + +Infra & Scheduling Team:统一 registry/capability center 字段、quota(按 user/channelId)、region·GPU·provider routing、model policy,以及本仓私有派发与统一调度的收敛路径。 diff --git a/docs/integration/audit-trace-schema.md b/docs/integration/audit-trace-schema.md new file mode 100644 index 0000000..8d1551f --- /dev/null +++ b/docs/integration/audit-trace-schema.md @@ -0,0 +1,75 @@ +# 审计与链路追踪 Schema(Audit / Lineage / Trace) + +> 状态:**DRAFT / 待对齐 Audit & Compliance Team**。 +> +> 依据:`heicode-mananger/docs/heicode.md §五/§九`、`docs/heicode-runtime-auth-newapi-secret-design.md §三`。配套:[`event-schema.md`](./event-schema.md)、[`security-boundary.md`](./security-boundary.md)。 + +## 1. 原则 + +- 审计要能回答:**谁、在什么时候、让哪个子 Agent、使用了什么资源、做了什么、结果如何**。 +- AGENT.md / resource context 面向模型理解;**permission manifest 面向系统强制执行**;审计以结构化记录为准,不以 Markdown 为准。 +- 审计记录**不得含明文密钥**:只记 `secret_ref`(`azkv://`)、审批 `approval_id`、范围与 TTL。 + +## 2. 追踪域(Lineage) + +### 2.1 部署 / 运行链路 +`manager_deployment_id ↔ deployment_id ↔ swarm_id(workflow_id) ↔ correlation_id(trace_id)`,四者贯穿一次交付,事件按 `swarm_id` 持久化。 + +### 2.2 任务链路(已实现) +每个任务携带:`task_id`、`parent_task_id`、`root_task_id`、`child_task_ids`、`depends_on`、`source`(`runtime_bridge` / `planner` / `dynamic_handoff`)、`agent_role`、`assigned_agent_id`、`retry_count`、时间戳。可由此重建任务 DAG 与重做/移交谱系。 + +### 2.3 执行链路(已实现,基于事件流) +按 `swarm_id` 顺序持久化的事件即执行轨迹:`task.created/claimed/running/heartbeat/blocked/retried/failed/completed`、`handoff.requested/completed`、`artifact.created`、`timeline.updated`、`deployment.status_changed`,每条含 `event_id`、`occurred_at`、`agent_instance_id`、`correlation_id`。 + +### 2.4 审批链路(已实现) +`run.approvals[approval_id]`:`operation`、`risk_level`、`decision`(approved/rejected)、`credential_ref`、`lease_id`、`reason`、决策时间。客户端审批,运行时仅记录与校验。 + +### 2.5 资源 / 凭证访问链路 +`secret_refs`(`azkv://`,仅引用)、resource grant 的 `allowed_actions`/`constraints`/`status`/`created_by`/`revoked_by`/`created_at`/`revoked_at`(字段定义在 Manager 侧 `heicode.md §五`)。本仓只透传与引用,不落明文。 + +### 2.6 回调投递链路(已实现) +`run.metadata.callback_attempts`:每次回调的 `event_id`、`event_type`、`url`、`status`、`attempted_at`,用于回调可靠性审计(`/diagnostics` 暴露)。 + +## 3. 已实现(本仓) + +| 能力 | 实现 | +|---|---| +| 事件流持久化 + 分页查询 | `_store_event`(按 `swarm_id` 追加)、`list_events`(`cursor`/`limit`);`/logs`、`/events` 暴露 | +| 任务谱系字段 | `parent/root/child_task_ids`、`depends_on`、`source`、`retry_count` | +| 审批记录 | `run.approvals`、`record_approval_decision` | +| 回调投递审计 | `callback_attempts`、`/diagnostics` | +| 用量归因 | `usage` + `X-Agent-*` 归因头(见 usage-billing) | +| 脱敏 | `_redact_sensitive`(保留 `secret_ref`,脱敏明文密钥) | + +## 4. 缺口 + +| 追踪项 | 状态 | +|---|---| +| Task lineage / execution trace | ✅ 已实现(事件流 + 任务字段) | +| Approval trace | ✅ 已实现 | +| Prompt trace(每次模型输入提示留痕) | 🔴 未实现(仅记 `model_id`/用量,不留 prompt 原文) | +| Model trace(请求/响应、参数、provider) | 🟡 部分(`model_id`/tokens;无完整请求响应留痕) | +| Tool trace(工具调用谱系) | 🔴 未实现(无 SK/MCP 工具计量,`sk_tool.*` 仅在 schema 预留) | +| 统一审计查询字段 / 留存策略 | 🟡 事件可查;标准查询字段与留存期未定义 | +| 交付回放(replay) | 🟡 事件流可顺序回放生命周期;无独立 replay 接口/快照格式 | + +## 5. 建议审计记录 Schema(草案,待 Audit Team 冻结) + +```jsonc +{ + "audit_id": "aud_...", + "occurred_at": "2026-06-08T...Z", + "actor": { "user_id": "...", "channel_id": "..." }, // 谁(按 user/channelId,非 tenant) + "subject": { "agent_role": "...", "agent_instance_id": "...", "task_id": "..." }, // 哪个子 Agent / 任务 + "action": "task.completed | approval.decided | resource.accessed | ...", + "resource": { "type": "git|database|storage|model", "ref": "non-secret-id", "secret_ref": "azkv://..." }, + "approval_id": "approval_... | null", + "lineage": { "manager_deployment_id": "...", "deployment_id": "...", "swarm_id": "...", "correlation_id": "..." }, + "result": "success | failed | blocked", + "redacted": true +} +``` + +## 6. 待对齐对象 + +Audit & Compliance Team:prompt/model/tool lineage 是否强制留痕及留存期、统一审计查询字段、交付回放快照格式、日志留存与合规要求;与 `telemetry-architecture`(benchmark 目录)数据源对齐。 diff --git a/docs/integration/event-schema.md b/docs/integration/event-schema.md new file mode 100644 index 0000000..0b1d779 --- /dev/null +++ b/docs/integration/event-schema.md @@ -0,0 +1,98 @@ +# Swarm 运行时事件 Schema(回调 / 事件流) + +> 状态:**已对齐 HM 实现**(依据 `heicode-mananger/heicode/controller/agent_callback.go`,handler `POST /api/agent/callbacks/runtime-events`)。本文是 Swarm → HM 回调与 `GET …/{id}/events` 事件流的统一 schema。 +> +> 配套:生命周期与签名见 [`runtime-contract.md`](./runtime-contract.md)。 + +## 1. 传输与鉴权 + +- Swarm → HM:`POST {callback.url}`(create 时下发)。 +- 鉴权二选一:服务令牌 `X-Agent-Service-Token`(或 `Authorization: Bearer`),或 HMAC 签名。 +- **HMAC 签名(已实现,与 HM 一致)**: + - 头:`X-Agent-Timestamp`(Unix 毫秒)、`X-Agent-Signature`、`X-Agent-Event-Id`、`X-Correlation-ID`(均含 `X-Agnet-` 兼容别名)。 + - 规范串:`canonical = f"{timestamp}.{event_id}.{raw_body}"`。 + - 签名:`X-Agent-Signature = "sha256=" + hex(HMAC_SHA256(secret, canonical))`。 + - secret:`AGENT_CALLBACK_SIGNING_SECRET`(兼容 `AGNET_…`,支持 `…_REF` 的 `azkv://` 解析)。 + - 时间容差:HM 默认 300s(`AGENT_CALLBACK_SIGNATURE_TOLERANCE_SECONDS`)。 +- **幂等去重**:HM 依次按 `X-Agent-Event-Id` 头 → body `event_id` → `idempotency_key`。重复返回 `{inserted:false, idempotent:true, deduplicated:true}`。 + +## 2. 事件 Envelope + +```jsonc +{ + "event_id": "evt-...", // 必填;去重主键 + "idempotency_key": "...", // 可选;缺省回退 event_id + "event_type": "task.completed", // 必填;取值见 §4 + "deployment_id": "runtime-dep-...",// 运行时部署 ID + "swarm_id": "swarm-...", // 工作流 ID(事件按此持久化) + "agent_instance_id": "...", // Agent 实例/角色实例 ID(可选) + "task_id": "...", // 任务相关事件填 + "occurred_at": "2026-06-08T...Z", // 事件时间 + "correlation_id": "corr-...", // 追踪 ID(X-Correlation-ID) + "source": "heicode-swarm-runtime", // 事件来源标识 + "metadata": { }, // 自定义元数据(不得含明文密钥) + "payload": { }, // 事件专属字段(见 §4) + "artifact": { } // 可选;见 §3 +} +``` + +## 3. Artifact 子对象(`artifact.created` 及完成事件可带) + +```jsonc +{ + "artifact_id": "art_...", + "artifact_type": "code_patch | document | deployment_manifest", + "title": "...", + "summary": "...", + "uri": "git://repo#branch | runtime://...", + "checksum": "", + "metadata": { "redacted": true, "agent_role": "...", "files_modified": [] } +} +``` + +## 4. 事件类型注册表(与 HM 一致) + +| event_type | payload 必填字段(HM 校验) | 分类 | Swarm 是否发出 | +|---|---|---|---| +| `deployment.status_changed` | `status` | deployment | ✅ | +| `phase.changed` | `stage` 或 `checkpoint` | ordinary_sub | ❌(暂未用) | +| `agent.started` | `agent_role` | ordinary_sub | ❌ | +| `agent.completed` | `agent_role` | ordinary_sub | ❌ | +| `agent.crashed` | `agent_role`, `reason` | ordinary_sub | ❌ | +| `task.created` | `task_id`, `title` | swarm_task_flow | ✅ | +| `task.claimed` | `task_id`, `agent_role` | swarm_task_flow | ✅ | +| `task.running` | `task_id`, `agent_role` | swarm_task_flow | ✅ | +| `task.heartbeat` | `task_id`, `agent_role` | swarm_task_flow | ✅ | +| `task.blocked` | `task_id`, `reason` | swarm_task_flow | ✅ | +| `task.retried` | `task_id`, `attempt` | swarm_task_flow | ✅ | +| `task.released` | `task_id`, `agent_role` | swarm_task_flow | ⚠️ 未发出(见 §6) | +| `task.failed` | `task_id`, `reason` | swarm_task_flow | ✅ | +| `task.completed` | `task_id` | swarm_task_flow | ✅ | +| `handoff.requested` | `task_id`, `from_role`, `to_role` | swarm_task_flow | ✅ | +| `handoff.completed` | `task_id`, `from_role`, `to_role` | swarm_task_flow | ⚠️ 缺 `from_role`/`to_role`(见 §6) | +| `approval.requested` | `approval_id`, `operation`, `risk_level` | approval | ✅ | +| `artifact.created` | `artifact_id` | artifact | ✅ | +| `timeline.updated` | `title` | timeline | ⚠️ 发出 `summary`,缺 `title`(见 §6) | +| `sk_tool.called` | `tool_name`, `tool_invocation_id` | sk | ❌(无 SK 工具) | +| `sk_tool.completed` | `tool_name`, `tool_invocation_id` | sk | ❌ | +| `sk_tool.failed` | `tool_name`, `tool_invocation_id`, `reason` | sk | ❌ | +| `budget.alert` | `threshold_pct` | budget | ⚠️ 发出 `threshold`,缺 `threshold_pct`(见 §6) | + +> 说明:评审/重做循环复用 `task.retried` + `timeline.updated`(`Review cycle N`)表达,无单独 review 事件类型;如 HM 前端需要独立 review 事件,列为待对齐项。 + +## 5. HM 响应 + +```json +{ "success": true, "event_id": "evt-...", "inserted": true, "idempotent": false, "deduplicated": false, "deployment_id": "dep_..." } +``` + +## 6. 已知对齐缺口(需在 Swarm 代码修正) + +下列为本仓 `orchestrator/` 当前发出字段与 HM 校验的差异,应在对应 emit 处修正后再宣称完全对齐: + +1. **`timeline.updated`**:HM 要求 `title`;当前发 `summary`。修正:payload 增加 `title`(可复用 `summary`)。 +2. **`budget.alert`**:HM 要求 `threshold_pct`;当前发 `threshold`(如 `0.8`)。修正:增加 `threshold_pct`(百分比,如 `80`)。 +3. **`handoff.completed`**:HM 要求 `from_role`/`to_role`;当前缺。修正:在 `finalize_parent_after_child` / handoff 完成处补 `from_role`/`to_role`。 +4. **`task.released`**:HM 注册表含此事件;`task_queue.release_task` 当前静默。修正:释放任务时发 `task.released`(`task_id`, `agent_role`)。 + +> 这些是确定性的小修,建议配 contract test(按 HM 必填字段断言每类事件 payload)。 diff --git a/docs/integration/frontend-event-api.md b/docs/integration/frontend-event-api.md new file mode 100644 index 0000000..c1c8be5 --- /dev/null +++ b/docs/integration/frontend-event-api.md @@ -0,0 +1,58 @@ +# 前端事件 API(Frontend Event API) + +> 状态:**DRAFT / 待对齐 Frontend & Product Team**。 +> +> 依据:`heicode-mananger/docs/integration/heicode-desktop-client-api.md`、`docs/integration/manager-side-contract-patches.md §6`。配套:[`event-schema.md`](./event-schema.md)、[`runtime-contract.md`](./runtime-contract.md)。 + +## 1. 现状(重要) + +- 当前**桌面端是单 Agent 模型**:用户部署「模板 Agent」,客户端从 HM 拿 `subdomain`+`access_token` 后**直连 Agent**走 A2A:`POST {subdomain}/message/stream`(SSE)/ `message/send`、`GET /.well-known/agent.json`、`/health`。HM 不在回路。 +- **HM 尚未定义「蜂群图 / agent 拓扑 / review loop / retry 时间线」的前端事件 API**。蜂群前端契约**归 Swarm + Frontend/Product Team 共同定义**,目前不存在。 +- 因此本文档区分:①HM 既有的任务卡片事件通道;②Swarm 现已提供、前端可消费的 REST;③待定义的蜂群前端事件 API。 + +## 2. HM 既有前端事件通道(任务卡片,非蜂群图) + +来自 `manager-side-contract-patches.md §6`: +- `GET /api/user/tasks/{id}/events`(SSE);HM 当前以**轮询兜底**(active 3s、其它状态 15s)。 +- 建议 SSE 事件名与载荷: + - `status_changed` → 新状态 + - `followup_added` → followup 载荷 + - `card_updated` → 完整 `HeicodeTaskCard`(`{goal, scope, generated_artifacts, manager_actions:[{label, deeplink}]}`) +- 这是**任务卡片**视角,不是蜂群 DAG/拓扑视角。 + +## 3. Swarm 现已提供的可消费 REST(已实现) + +前端(经 HM 或直连受控读接口)可消费: + +| 接口 | 用途 | 关键字段 | +|---|---|---| +| `GET …/{deployment_id}/workflow` | 工作流/阶段视图 | `status`、`summary`、`agent_count`、`tokens`、`tools`、`elapsed_seconds`、`phases[]`、`artifacts[]` | +| `GET …/{deployment_id}/tasks` | 任务 DAG | 每任务 `task_id`、`agent_role`、`status`、`depends_on`、`parent_task_id`、`root_task_id`、`attempt`、`blocked_reason` | +| `GET …/{deployment_id}/logs`(`/events`) | 事件流 | 事件 envelope(见 event-schema),分页 `cursor`/`limit` | +| `GET …/{deployment_id}/metrics` | 指标 | `tasks_by_status`、`agents_connected`、`average_task_duration_seconds`、`budget` | +| `GET …/{deployment_id}/diagnostics` | 诊断 | 失败任务、回调尝试、审批 | + +`phases[]` 形如:`{phase_id, name, status, agents[]}`,`name ∈ {Plan, Dispatch, Execute, Handoff, Review, Deliver}`;`agents[]` 形如 `{agent_id, name, role, status, tokens, tools, elapsed_seconds, artifact_ids}`。 + +> 注意:当前**仅 REST 拉取**,Swarm 未向前端推送 SSE/WS。前端可轮询 `/workflow` + `/logs`(用 `cursor` 增量)。 + +## 4. 目标:蜂群前端事件 API(待定义) + +为支撑工单要求的 swarm graph / agent topology / task DAG / review loop / retry timeline / token-cost monitor / collaboration timeline,建议(待 Frontend/Product Team 冻结): + +| UI 视图 | 数据来源(现有) | 缺口 | +|---|---|---| +| Task DAG / topology | `/tasks`(`depends_on`)+ `/workflow.phases` | 稳定 graph schema、增量更新事件 | +| Review loop / retry timeline | `task.retried` + `timeline.updated`(`Review cycle N`) | 独立 review/retry 事件类型与时间线 schema | +| Collaboration timeline | `handoff.requested/completed` + peer 路由 | peer 消息事件未对前端暴露 | +| Token / cost monitor | `/metrics` + `budget.alert`(usage) | 统一 usage 推送(见 usage-billing §7) | +| 实时推送 | `/logs` 轮询 | 统一 SSE/WS event stream(事件名、断线重连、刷新可恢复) | + +建议事件流形态(草案):SSE `GET …/{deployment_id}/stream`,事件名复用 `event-schema` 的 `event_type`,载荷为对应 envelope;前端按 `swarm_id` 渲染、按 `event_id` 去重、断线后用 `/logs?cursor=` 回补。 + +## 5. 缺口与待对齐 + +- 🔴 Swarm 未提供前端 SSE/WS 推送(仅 REST 轮询)。 +- 🔴 蜂群 graph/timeline/cost 的统一前端 schema 未定义。 +- 🟡 review/retry/collaboration 缺独立事件类型(当前借 `task.retried`/`timeline.updated`)。 +- 待对齐:Frontend / Product Team 确认蜂群三视图(graph / timeline / cost)入口与事件流形态;与 HM 的 `/api/user/tasks/{id}/events` 通道如何衔接。 diff --git a/docs/integration/runtime-contract.md b/docs/integration/runtime-contract.md new file mode 100644 index 0000000..3b680ef --- /dev/null +++ b/docs/integration/runtime-contract.md @@ -0,0 +1,96 @@ +# Manager ↔ Swarm Runtime Contract(Swarm 侧拥有) + +> 状态:**草案(Swarm 侧拥有,待 HM 对齐)** · 对齐对象:Heicode Manager Runtime Team +> +> 依据: +> - HM 侧裁定 `heicode-mananger/docs/integration/heicode-swarm-deferred.md`:**HM 当前不实现 swarm runtime**;多 Agent 蜂群归 `agent_swarm` / `HeiCode-Swarm`(本仓);HM 侧待本仓给出正式 `/api/agent/swarm/*` 接口后,在 HM 的 `docs/integration/` 另立契约跟踪。 +> - 既有运行时集成范式 `heicode-mananger/docs/integration/heicode-am-contract.md`(单 Agent 模板 Agent,经 AM 启动)。本契约沿用其鉴权、回调、env、路径覆盖等约定。 +> +> 本文是 HM 侧 deferred 锚点所要求的「Swarm 侧正式接口」起草。冻结需 Manager Runtime Team 评审。 + +## 1. 角色与边界 + +- **HM(Heicode Manager)**:控制面。负责用户输入、资源/权限/计费/审计、模型网关(`/v1/*`)。HM **不**承载蜂群编排。 +- **Swarm Runtime(本仓)**:执行面。接收 HM 下发的编排请求,分解→派发→执行→评审→汇总,并通过**带签名回调**回报生命周期事件。 +- HM 不在对话/执行回路;Swarm 通过回调把状态推回 HM,HM 也可主动拉取(见 §4)。 + +## 2. 鉴权(沿用 AM 约定) + +- HM → Swarm:`Authorization: Bearer `。Swarm 侧校验 `AGENT_RUNTIME_SERVICE_TOKEN`(兼容 `AGNET_RUNTIME_SERVICE_TOKEN`)。未配置时为**非安全开发模式**(仅本地)。 +- 统一响应信封:成功 `{ "success": true, "data": {...} }`;失败 `{ "success": false, "error": { "code", "message", "request_id" } }`。 +- 常用请求头:`X-Correlation-ID`、`X-Idempotency-Key`、`Authorization`。 + +## 3. 生命周期接口(已实现 / 待对齐) + +Swarm 暴露以下接口(三组别名等价,便于 HM 路径覆盖): +`/api/swarms`、`/api/agent/swarm/deployments`、`/api/agnet/deployments`。 + +| 动作 | 方法 + 路径 | 状态 | 说明 | +|---|---|---|---| +| **create** | `POST /api/agent/swarm/deployments` | ✅ 已实现 | 创建蜂群部署;幂等键 `X-Idempotency-Key` | +| **status** | `GET …/{deployment_id}` | ✅ 已实现 | 返回部署概要与状态 | +| task graph | `GET …/{deployment_id}/tasks` | ✅ 已实现 | 任务 DAG | +| logs / events | `GET …/{deployment_id}/logs`、`/events` | ✅ 已实现 | 事件流(分页 `cursor`/`limit`) | +| metrics / workflow / diagnostics | `GET …/{deployment_id}/metrics`、`/workflow`、`/diagnostics` | ✅ 已实现 | 指标与编排视图 | +| **cancel** | `POST …/{deployment_id}/stop` | ✅ 已实现 | 停止并取消非终态任务,向 Agent 下发 `cancel_task` | +| **approve** | `POST …/{deployment_id}/approvals/{approval_id}` | ✅ 已实现 | 接收 Manager 审批决定(approved/rejected) | +| **resume** | — | 🟡 待对齐 | 当前仅「审批通过」隐式恢复(approvals);无独立 resume 端点 | +| **retry** | — | 🟡 待对齐 | 任务级重试 / 评审重做为内部机制;无外部 retry 端点 | + +### 3.1 create 请求(必填校验) +必填:`orchestration_plan.objective`、`callback.url`、`metadata.manager_deployment_id`。 +约束:`mode` 必须为 `swarm`;`billing_context.secret_ref`(若有)必须 `azkv://` 前缀;`resource_grants`/`metadata`/`callback` 不得含明文密钥。 + +create 响应 `data`:`deployment_id`、`runtime_deployment_id`、`manager_deployment_id`、`swarm_id`、`mode`、`status`、`runtime_execution_status`、`created`。 + +### 3.2 ID 语义(回应 HM 标记的 `deployment_id ↔ swarm_id` 缺口) +- `deployment_id` / `runtime_deployment_id`:Swarm 侧运行时部署 ID(`runtime-dep-…`)。 +- `swarm_id`:工作流 ID(`swarm-…`),作为 `workflow_id`;事件按 `swarm_id` 持久化与查询。 +- `manager_deployment_id`:HM 侧部署 ID(请求 `metadata.manager_deployment_id` 透传)。 +- `correlation_id`:贯穿一次交付的追踪 ID(`X-Correlation-ID`),作为 `trace_id`。 +- 三者映射在 Swarm 持久化中维护:`deployment_id ↔ swarm_id ↔ manager_deployment_id`,`get_run_by_identifier` 支持三者任一查询。 + +> HM 侧 `heicode-swarm-deferred.md` 记录的「Swarm 仅暴露 `/tasks`、缺 `deployment_id↔swarm_id`」为旧状态;本仓 v5/v6 已实现上述映射与 `/api/agent/swarm/*` 接口,需 HM 复核更新该锚点。 + +## 4. 状态机 + +部署状态:`waiting_approval` → `running` →(`blocked` ⇄ `running`)→ 终态 `completed` / `failed` / `stopped`。 + +| 状态 | 含义 | 进入方式 | +|---|---|---| +| `waiting_approval` | 高危/需审批,等待 Manager 审批 | create 时命中审批条件 | +| `running` | 任务派发与执行中 | 审批通过 / 有在途任务 | +| `blocked` | 任务因移交/依赖阻塞 | 子任务 `blocked_on_handoff` | +| `completed` | 所有任务终态且评审通过 | 全部完成(评审循环可选) | +| `failed` | 存在失败且不可恢复 | 任务终态含 failed | +| `stopped` | Manager 主动停止 | `…/stop` | + +每次状态变更通过回调 `deployment.status_changed` 推送(见 §5)。 + +## 5. 回调(已与 HM 处理器对齐) + +Swarm → HM:`POST {callback.url}`(create 时下发,默认 HM 的 `/api/agent/callbacks/runtime-events`)。 + +**鉴权与签名(与 HM `agent_callback.go` 一致,已实现):** +- 头:`X-Agent-Service-Token`(或 `Authorization: Bearer`)、`X-Agent-Timestamp`(Unix 毫秒)、`X-Agent-Signature`、`X-Agent-Event-Id`、`X-Correlation-ID`(均含 `X-Agnet-` 兼容别名)。 +- 签名:`signature = "sha256=" + hex(HMAC_SHA256(secret, f"{timestamp}.{event_id}.{raw_body}"))`。 +- secret:`AGENT_CALLBACK_SIGNING_SECRET`(兼容 `AGNET_…`);HM 侧容差默认 300s。 +- 幂等:HM 按 `X-Agent-Event-Id` → body `event_id` → `idempotency_key` 去重。 + +事件 envelope 字段与 event_type 取值见 [`event-schema.md`](./event-schema.md)(与 HM 注册表一致)。 + +## 6. 路径与超时覆盖(沿用 AM 约定) + +HM 侧可用 env 覆盖:`AGENT_RUNTIME_BASE_URL`、`AGENT_RUNTIME_SERVICE_TOKEN`、`AGENT_RUNTIME_AGENT_START_PATH`、`AGENT_RUNTIME_AGENT_PATH`、`AGENT_RUNTIME_AGENT_STOP_PATH`、`AGENT_RUNTIME_START_TIMEOUT_SECONDS`、`AGENT_RUNTIME_CALLBACK_URL`。 + +## 7. 安全 + +- `env`/请求体不得含明文密钥;凭据经 `secret_ref`(`azkv://`)注入。详见 [`security-boundary.md`](./security-boundary.md)。 +- Swarm 启动接口必须 HTTPS / 私网;回调走 HTTPS。 + +## 8. 待对齐项(冻结前需 Manager Runtime Team 确认) + +- [ ] resume / retry 是否需要独立外部端点,还是沿用 approvals + 内部重做。 +- [ ] HM 是否以 AM 同款 `/agents` 生命周期(而非 `/api/agent/swarm/*`)调用 Swarm;若是,需路径映射。 +- [ ] `event-schema` 中各 event_type 的必填字段与 HM 注册表逐项核对(见 event-schema.md)。 +- [ ] 更新 HM 侧 `heicode-swarm-deferred.md` 锚点,登记本契约。 diff --git a/docs/integration/security-boundary.md b/docs/integration/security-boundary.md new file mode 100644 index 0000000..7cbd81c --- /dev/null +++ b/docs/integration/security-boundary.md @@ -0,0 +1,85 @@ +# 安全边界(Secret / Workspace / Tool / Approval / Tenant / Sandbox) + +> 状态:**部分已实现,部分待接入**(依据 `heicode-mananger/docs/heicode.md §六/七`、`docs/heicode-runtime-auth-newapi-secret-design.md §三/五`、`docs/integration/heicode-am-contract.md §3.1/§4`)。 +> +> 配套:[`runtime-contract.md`](./runtime-contract.md)、[`usage-billing-schema.md`](./usage-billing-schema.md)。 + +## 1. 不可破坏原则 + +- 密钥/Token/云凭据/SSH 私钥/数据库密码/NewAPI key **不得**进入代码、日志、Markdown、前端响应或 Git。 +- 凭据基线 = **Azure Key Vault**;引用一律 `azkv:///secrets/`,**不向后兼容 `vault://`**。 +- Manager DB 与 Swarm 只保存 `secret_ref`,真实凭证由 Secret Broker 写入 Key Vault。 +- 子 Agent 不持有长期密钥;只接收角色、资源元数据、AGENT.md 与**短期、最小权限、可审计**凭证。 +- 高危操作审批**只在客户端完成**;运行时只校验审批结果,不发起审批。 + +## 2. Secret 注入边界 + +| 项 | 标准 | 本仓状态 | +|---|---|---| +| `secret_ref` 前缀 `azkv://` 强校验 | 必须 | ✅ `swarm_runtime.validate_create_request` 校验 `billing_context.secret_ref` 等为 `azkv://` | +| 拒绝明文密钥进入请求 | 必须 | ✅ `_reject_plaintext_secrets`(`metadata`/`resource_grants`/`callback`) | +| 响应/事件/持久化脱敏 | 必须 | ✅ `_redact_sensitive` 将明文密钥键(`password`/`token`/`secret`/`private_key`/`access_key` 及 `*_token`/`*_secret`/`*_password`/`*_key`)脱敏为 `[redacted]`;**保留** `secret_ref`/`credential_ref`/`signing_secret_ref`(安全引用) | +| `.env`/密钥不入库 | 必须 | ✅ `.gitignore` 忽略 `.env`、`secrets/`、`*.pem/key/p12/pfx`、`id_rsa/ed25519` | +| 真实凭证写入 Key Vault(Secret Broker) | Manager 侧 | ⛔ 非本仓(Manager Secret Broker 负责) | +| 短期凭证派生与注入 | 客户端审批后 | 🟡 Swarm 接收 `secret_context`;K8s 派生/注入由 Agent 平台(AM)实现 | + +`secret_context`(HM 下发,仅引用与审批结果): +```jsonc +"secret_context": { + "secret_refs": ["azkv://heicode-kv.vault.azure.net/secrets/res_git_1"], + "inject_short_lived_credentials": true, + "approval_id": "approval_123" +} +``` + +## 3. Workspace 隔离 + +- 每个任务在**独立按任务工作目录**执行(agent `task_workspace`)。 +- 文件写入有**路径越界校验**(`task_executor._resolve_workspace_path`,拒绝绝对路径与逃逸 workspace)。 +- Git 操作在仓库根(`repo_root`)执行,限定结果分支。 +- 🟡 待接入:跨任务/跨租户的强隔离、只读挂载、allowed_paths 强制(当前由模型提示约束,未做运行时强制)。 + +## 4. Tool / MCP 权限边界 + +- 当前 Agent 工具能力 = 工作区内文件读写 + Git;**无** SK/MCP 工具权限引擎。 +- 🟡 待接入:统一 tool/MCP permission boundary、allowed/denied 工具策略、敏感工具审批联动(`sk_tool.*` 事件已在 schema 预留)。 + +## 5. 审批门(Approval Gate) + +- 高危操作审批**只在客户端**;Swarm 不发起审批。 +- Swarm 实现:create 命中高危(`risk_level=high` 或 `requires_user_approval`)→ 进入 `waiting_approval`,发 `approval.requested`(`approval_id`/`operation`/`risk_level`);客户端经 Manager 审批后回 `POST …/approvals/{approval_id}`(approved/rejected)→ 恢复或阻断。 +- 🟡 待接入:审批主体/范围/TTL/`credential_ref`/`lease_id` 的逐项校验与到期失效,需与 Manager 审批链对齐。 + +## 6. Agent 鉴权与租户隔离 + +- **Swarm 模型**:Agent 主动出站连编排器 WebSocket(`/ws/{agent_id}`),**不**对公网暴露每 Agent 子域名。AM 单 Agent 模型里的「客户端↔agent 直连 + `AGENT_ACCESS_TOKEN` 本地校验」**不适用于** swarm(无直连回路)。 +- 服务间鉴权:HM→Swarm 用 `AGENT_RUNTIME_SERVICE_TOKEN`(Bearer);回调 HMAC 签名。 +- 🟡 待接入:多租户运行时隔离(命名空间/网络/配额)由 Agent 平台(AKS Workload Identity)承载,非本仓编排器;归因主轴为 `user.id`/`channelId`(见 `usage-billing-schema.md`),不引入 tenant 概念。 + +## 7. 外部 API 与传输 + +- 模型调用统一走 HM `/v1`(OpenAI 兼容),用 HM 现签 `OPENAI_API_KEY`。 +- 传输:编排器/Agent 接口与回调走 **HTTPS / 私网**;`env` 含明文密钥时启动接口必须 HTTPS(`heicode-am-contract §4`)。 +- 🟡 待接入:统一 external API egress 策略(白名单/出网控制)。 + +## 8. 执行沙箱 + +- 当前执行单元为进程 / K8s Pod,隔离强度依赖部署(namespace/资源限额)。 +- 🟡 待接入:强化沙箱(seccomp/只读根/网络策略/能力裁剪),由 Infra/Security Team 定义。 + +## 9. 覆盖与缺口 + +| 边界 | 状态 | +|---|---| +| `azkv://` `secret_ref` 强校验 / 明文拒绝 / 脱敏 / `.gitignore` | ✅ 已实现(本仓) | +| Workspace 路径越界校验 | ✅ 已实现 | +| 审批状态机(waiting_approval + approvals 回执) | ✅ 已实现(逐项校验待加强) | +| 短期凭证派生注入、Workload Identity | 🟡 AM/K8s 侧 | +| Tool/MCP 权限引擎 | 🔴 未实现 | +| allowed_paths 运行时强制、强隔离 | 🔴 未实现 | +| 强化执行沙箱 | 🔴 未实现 | +| 租户隔离 | ⛔ 不在本仓(归因按 user/channelId) | + +## 10. 待对齐对象 + +Security / Governance Team(tool/MCP 边界、沙箱、审批逐项校验)、Infra Team(Workload Identity、租户隔离、egress 策略)。 diff --git a/docs/integration/usage-billing-schema.md b/docs/integration/usage-billing-schema.md new file mode 100644 index 0000000..597716c --- /dev/null +++ b/docs/integration/usage-billing-schema.md @@ -0,0 +1,108 @@ +# 用量与计费 Schema(Usage / Billing) + +> 状态:**已对齐既有边界**(依据 `heicode-mananger/docs/heicode.md §八`、`docs/heicode-runtime-auth-newapi-secret-design.md §四/五`、`docs/integration/Heicode-Manager-PayPal支付接入与计费关系说明.md §3/§5`)。 +> +> 配套:事件载体见 [`event-schema.md`](./event-schema.md)(`budget.alert`);安全见 [`security-boundary.md`](./security-boundary.md)。 + +## 1. 计费主线原则(不可破坏) + +- **NewAPI 是计费账本**:模型调用费用在 HM 模型网关 `/v1/*` 处**按请求计量并扣费**(NewAPI 的 user / token / group / quota / usage)。**Swarm 不是账本**,也不重算扣费。 +- **Swarm 的职责 = 用量上报与预算约束**,用于归因、观测与预算告警,不作为扣费依据。 +- **归因主轴 = `user.id` / `channelId` / NewAPI user/token/group**,**不是** `tenant`/`project`。`heicode-runtime-auth §六` 明确:当前不引入 tenant/project 作为计费主轴。 +- Agent 基础设施成本(CPU/内存/Pod)当前**未形成用户账本**(PayPal 说明 §5)。 + +> ⚠️ 与工单口径的差异:工单要求「tenant attribution」;按 Manager 标准应改为 **user/channelId 归因**。如未来确需租户账本,需作为独立产品决策,不在本仓暗自添加。 + +## 2. 计费归因上下文(HM 下发,Swarm 透传) + +来自 `heicode-runtime-auth §五`: + +```jsonc +"billing_context": { + "provider": "newapi", + "newapi_user_ref": "newapi_user_123", + "newapi_group": "development", + "quota_ref": "newapi_token_or_group_quota_ref" +} +``` + +- Agent 调模型走 HM `/v1`,用 **HM 为该用户现签的 `OPENAI_API_KEY`(sk-)**,扣费即记在该用户名下;删 agent 时吊销。 +- Swarm 的 task_executor 随模型请求带 **`X-Agent-*` / `X-Agnet-*` 归因头**(`manager_deployment_id`、`swarm_id`、`task_id`、`agent_role`、`correlation_id`、`model_id`),供 HM/NewAPI 关联归因。 + +## 3. 预算约束(orchestration_plan.budget) + +来自 PayPal 说明 §5: + +| 字段 | 含义 | +|---|---| +| `budget.max_tokens` / `token_limit` | 本次部署 token 上限 | +| `budget.max_cost_usd` | 本次部署美元成本上限 | +| `budget.duration_seconds` / `max_duration_seconds` | 运行时长上限 | + +Swarm 在运行中按时长/成本比例发 `budget.alert`(默认 80% 阈值),并在 `metrics` 返回预算占比。**这是约束与告警,不是扣费。** + +## 4. 用量上报 Schema + +### 4.1 Agent 单任务用量(task 结果 `usage`,已实现) +```jsonc +"usage": { + "model_id": "gpt-4o-mini", + "model_tokens": 0, + "prompt_tokens": 0, + "completion_tokens": 0, + "model_cost_usd": 0.0, + "runtime_seconds": 0.0, + "billing_source": "newapi | unknown" +} +``` + +### 4.2 运行级用量事件(orchestrator,已实现) +通过 `budget.alert` 事件上报(payload 含): +```jsonc +{ + "model_id": "...", "model_tokens": 0, "prompt_tokens": 0, "completion_tokens": 0, + "model_cost_usd": 0.0, "runtime_seconds": 0.0, "billing_source": "...", + "manager_deployment_id": "...", "swarm_id": "...", "task_id": "...", + "agent_role": "...", "correlation_id": "...", + "budget": { "max_tokens": null, "max_cost_usd": null, "consumed_usd": 0.0, "remaining_usd": null } +} +``` + +### 4.3 目标完整回传形态(对齐 AM,部分待实现) +PayPal 说明 §5 给出的运行时用量回传目标: +```jsonc +{ + "deployment_id": "dep_xxx", "agent_instance_id": "agi_backend_001", + "usage": { + "model_tokens": 32000, "model_cost_usd": 4.21, "runtime_seconds": 930, + "cpu_core_seconds": 1200, "memory_mb_seconds": 2048000 + }, + "billing_source": "newapi", "correlation_id": "corr_xxx" +} +``` + +## 5. 多 Agent / 评审重做 成本聚合 + +- 一次 swarm 请求拆成多 task / 多 agent / 多评审轮;**每个 task 的 `usage` 可按 `swarm_id` 聚合**得到运行级合计(观测用途)。 +- **评审重做成本**:每轮重做都会重新执行任务并累计 `usage`,因此**已隐含计入**运行级合计;但当前**未单独打标**「review_retry 成本」。 + +## 6. 字段覆盖与缺口 + +| 字段 | 状态 | +|---|---| +| `model_tokens` / `prompt_tokens` / `completion_tokens` | ✅ 已采集 | +| `model_cost_usd`、`runtime_seconds`、`billing_source` | ✅ 已采集 | +| 归因:`manager_deployment_id`/`swarm_id`/`task_id`/`agent_role`/`correlation_id` | ✅ 已采集 | +| `reasoning_tokens` / `cache_tokens` | 🔴 未采集(取决于 provider usage 返回) | +| `tool_cost`(工具调用成本) | 🔴 未采集(无 SK 工具计量) | +| `cpu_core_seconds` / `memory_mb_seconds`(基础设施) | 🔴 未采集(K8s 指标未接入账本) | +| `review_retry` 成本单独打标 | 🟡 隐含累计,未单独标注 | +| provider 成本拆分 | 🟡 由 NewAPI/账本侧负责,非本仓 | +| tenant 归因 | ❌ 按标准不使用(见 §1) | + +## 7. 待对齐项 + +- [ ] 运行级用量是否需独立 `usage.report` 事件类型(当前借 `budget.alert` 载体);与 HM/Billing Team 确认(HM 注册表暂无 usage 事件)。 +- [ ] `budget.alert` 需补 `threshold_pct`(见 `event-schema.md §6`)。 +- [ ] 若需基础设施计费,接入 K8s 用量(cpu/memory)并由 Billing Team 定义资源计价。 +- [ ] `review_retry` 成本是否需单独打标,由 Billing Team 决定。 diff --git a/docs/peer-communication-logistics.md b/docs/peer-communication-logistics.md new file mode 100644 index 0000000..527c214 --- /dev/null +++ b/docs/peer-communication-logistics.md @@ -0,0 +1,117 @@ +# Peer Communication Logistics(对等通信机制与深化设计) + +> 状态:**现状 + 深化设计**。描述专家 Agent 之间「就重叠领域相互沟通」的消息机制、路由逻辑、当前局限,以及深化方案与分阶段计划。 +> +> 相关代码:`agent/main.py`(`request_peer_collaboration` / `handle_peer_message` / `answer_peer_query`)、`orchestrator/main.py`(`peer_message` 路由 / `build_dispatch_context`)、`agent/task_executor.py`(`_execute_subtask` 触发)。相关文档:[`event-schema`](./integration/event-schema.md)、[`frontend-event-api`](./integration/frontend-event-api.md)、[`benchmark/swarm-metrics-schema`](./benchmark/swarm-metrics-schema.md)(`S_communication`)。 + +--- + +## 1. 当前实现(mechanics) + +### 1.1 消息信封 +```jsonc +{ + "type": "peer_message", + "agent_id": "发送方", + "target_agent_id": "接收方", + "from_agent_id": "由编排器转发时补盖(接收方据此回复)", + "task_id": "...", + "content": "文本内容", + "correlation_id": "peer--", // 关联请求与回复 + "is_reply": false, // true = 回复 + "timestamp": 0.0 +} +``` + +### 1.2 路由(编排器为 broker) +Agent 出站连编排器 WebSocket,彼此不直连。编排器收到 `peer_message` → 按 `target_agent_id` 转发,并**补盖 `from_agent_id`**(发送方身份)。无 `target_agent_id` 则记告警丢弃。 + +### 1.3 请求/回复(一问一答) +- 发起方 `request_peer_collaboration(task_id, target_agent_id, content, timeout)`:生成 `correlation_id`,挂一个 `asyncio.Future` 到 `peer_waiters[correlation_id]`,发出 query,`await asyncio.wait_for(waiter, timeout)`。 +- 接收方 `handle_peer_message`:若 `correlation_id` 命中自身 waiter(或 `is_reply`)→ 解析为「回复」并 resolve waiter;否则视为「入站查询」→ `answer_peer_query`。 +- `answer_peer_query`:**已实现实质性回复**——对查询做一次受限 LLM 推理(`TaskExecutor.peer_reply`,受 `PEER_CONSULT_MAX_TOKENS`,默认 500 约束),结合自身 workspace 产物,返回结构化 `{stance, content, evidence, refs}`;无模型/超时/失败时回退到 `last_summary` 摘要。回复在后台任务中完成,不阻塞消息循环(`PEER_REPLY_TIMEOUT_SECONDS`,默认 20s)。 + +### 1.4 触发与编排(logistics) +- 派发时 `build_dispatch_context` 向任务上下文注入:`peer_agents`(本次 run 中**已连接**的其它 Agent:`agent_id`/`role`/`capabilities`)与 `dependency_artifacts`(已完成依赖产物)。 +- `task_executor._execute_subtask` 仅当 `specialist_role ∈ {testing, documentation}` 且存在 `peer_agents` 且提供了回调时发起咨询;按 `prefer implementation` 排序,取前 `max_peer_consults`(默认 2)个,`peer_timeout_seconds`(默认 10s)。 +- 对齐原则(提示词内):实现产物为 API/异常语义的 source of truth;peer 输入与实现冲突时以实现为准并在变更摘要中说明。 + +--- + +## 2. 当前局限(为何"浅") + +| 局限 | 说明 | +|---|---| +| ~~回复浅~~ ✅ 已深化 | `answer_peer_query` 现做受限 LLM 推理 + 引用 workspace 产物 + 结构化字段(`stance/content/evidence/refs`);无模型时回退摘要(见 §3.3) | +| 触发窄 | 仅 testing/documentation 角色发起;实现角色不主动协作 | +| 单轮 | 一问一答,无多轮/澄清/协商线程 | +| 无广播 | 只能点对点 `target_agent_id`,无按角色/能力的群发或发现 | +| 无遥测 | 消息未计数(成功/失败/总数)→ `S_communication` 不可算(见 metric-coverage-gaps) | +| 弱冲突处理 | 仅"以实现为准"的提示约定,无结构化协商/升级协议 | +| 无前端可见 | peer 消息未作为事件暴露,前端协作时间线无数据 | + +--- + +## 3. 深化设计(target) + +### 3.1 消息类型(扩展 `kind`) +`query`(提问)·`reply`(回答)·`clarify`(追问)·`proposal`(提案)·`critique`(质疑)·`broadcast`(群发)·`ack`(确认)。 + +### 3.2 多轮线程 +增加 `conversation_id` + `turn`;`peer_waiters` 升级为按会话的队列,支持 `clarify` 往返与超时续约,受 `max_rounds` 约束。 + +### 3.3 实质性回复 ✅ 已实现 +`answer_peer_query` 已由「回 last_summary」升级为:**针对 query 生成有据回复**(`TaskExecutor.peer_reply` 做一次受限推理,引用自身 workspace 产物),返回结构化 `{stance, content, evidence, refs}`,受 `PEER_CONSULT_MAX_TOKENS`(默认 500)约束;无模型/失败回退摘要;后台任务回复不阻塞消息循环。由 `scripts/test-merge-smoke.py` 覆盖(fallback + 实质回复两条断言)。 + +### 3.4 寻址与发起 +- 点对点(`target_agent_id`)+ **按角色/能力寻址**(编排器按注册表解析)+ 受控**广播**。 +- 任何角色均可发起咨询(不限 testing/doc),由预算与策略限制频次。 + +### 3.5 冲突解决协议 +`critique`/`proposal` 往返;默认「实现为 source of truth」;**僵局上交 Master Agent**(`master_agent` 仲裁,纳入评审决策),而非各 Agent 私下定夺。 + +### 3.6 遥测(接入 benchmark) +编排器对每条 `peer_message` 计数:`total` / `delivered` / `failed`(接收方离线/超时)。 +- 产出 `S_communication = SuccessfulMessages / TotalMessages`(见 swarm-metrics-schema)。 +- 作为协作事件暴露(见 §4),供前端协作时间线与审计。 + +### 3.7 预算与可靠性 +`max_peer_consults`、`peer_timeout_seconds`、`max_rounds`、`peer_consult_max_tokens`;接收方离线 → 立即回 `failed` 而非干等;`correlation_id` 幂等去重。 + +--- + +## 4. 事件与可见性 + +为满足前端与审计,建议把对等协作纳入事件流(与 [`event-schema`](./integration/event-schema.md) 对齐): +- 新增/复用事件:`collaboration.message`(含 `from`/`to`/`kind`/`conversation_id`/`correlation_id`,内容脱敏)、`collaboration.resolved`/`collaboration.escalated`。 +- 前端按 `conversation_id` 渲染协作时间线(见 frontend-event-api §4)。 +- HM 注册表暂无 `collaboration.*` 事件 → 列为与 Workflow/Frontend Team 的待对齐项。 + +--- + +## 5. 与其它机制的关系 + +| 机制 | 作用 | 区别 | +|---|---|---| +| `dependency_artifacts` | 顺序依赖:下游看到上游产物 | 单向、派发时注入,非交互 | +| handoff | 把子任务委派给更合适的专家 | 转移所有权,非对等沟通 | +| peer communication | 重叠领域的对等交流 | 双向、运行中、不转移所有权 | +| Master Agent 评审 | 全局裁决是否达标 | 中心化决策,非点对点 | + +--- + +## 6. 分阶段计划 + +1. **遥测先行(低成本)**:编排器对 `peer_message` 计数 → 点亮 `S_communication`;把消息作为 `collaboration.message` 事件暴露。 +2. **实质回复** ✅ 已实现:`answer_peer_query` 改为受限 LLM 推理 + 引用产物(替换 last_summary)。 +3. **多轮 + 寻址**:`conversation_id`/`clarify`、角色/能力寻址、广播。 +4. **冲突协议 + 升级 Master**:`critique`/`proposal`,僵局上交 `master_agent`。 +5. 与 Frontend/Workflow Team 冻结 `collaboration.*` 事件与协作时间线契约。 + +--- + +## 7. 待对齐 + +- `collaboration.*` 事件类型与载荷(Workflow/Frontend Team;当前 HM 注册表未含)。 +- 对等咨询的预算口径(`max_rounds`/token)与频次策略(Governance)。 +- 实质回复是否计入用量/计费(每次 peer consult 是一次模型调用 → 见 usage-billing §5)。 diff --git a/orchestrator/main.py b/orchestrator/main.py index f3a66d3..6b289d1 100644 --- a/orchestrator/main.py +++ b/orchestrator/main.py @@ -24,6 +24,7 @@ from .handoff_manager import handoff_manager, HandoffRequest from .task_queue import task_queue, TaskStatus from .swarm_runtime import RuntimeValidationError, swarm_runtime from .planner import planner +from .master_agent import master_agent # Configure logging logging.basicConfig( @@ -217,7 +218,7 @@ async def maybe_run_review_cycle(run, tasks) -> bool: return False results = {t.task_id: {"result": parse_task_result(t) or {}} for t in completed} - verdict = await planner.review( + verdict = await master_agent.review_and_decide( run.objective, [task_payload(t) for t in completed], results, @@ -382,7 +383,7 @@ async def refresh_swarm_run_status(run): # Synthesize the specialist results into one coherent, user-facing answer. completed = [t for t in tasks if t.status == TaskStatus.COMPLETED] results = {t.task_id: {"result": parse_task_result(t) or {}} for t in completed} - final_summary = await planner.synthesize(run.objective, results) + final_summary = await master_agent.synthesize(run.objective, results) run.metadata["final_summary"] = final_summary await swarm_runtime.save_run(run) @@ -538,7 +539,7 @@ async def build_planner_task_specs(run, body: Dict[str, Any], base_specs: List[D """ objective = run.objective or "Complete swarm objective" base_context = (base_specs[0].get("context") if base_specs else {}) or {} - subtasks = await planner.build_plan(run.swarm_id, objective) + subtasks = await master_agent.plan(run.swarm_id, objective) prefix = f"{run.swarm_id}-" @@ -1017,6 +1018,8 @@ async def finalize_parent_after_child(run, child_task, agent_id: str, success: b **payload, "child_task_id": child_task.task_id, "parent_task_id": parent_task.task_id, + "from_role": parent_task.agent_role, + "to_role": child_task.agent_role, }, ) await swarm_runtime.emit_event( @@ -1896,6 +1899,10 @@ async def websocket_endpoint(websocket: WebSocket, agent_id: str): "source_agent_id": handoff.source_agent_id, "target_agent_id": handoff.target_agent_id, "task_id": task_id, + # HM requires from_role/to_role; this legacy accept path + # lacks role context, so fall back to agent ids. + "from_role": handoff.source_agent_id, + "to_role": handoff.target_agent_id, }, ) @@ -2130,7 +2137,22 @@ async def websocket_endpoint(websocket: WebSocket, agent_id: str): logger.info( f"Agent {agent_id} rejected task {rejected_task_id} ({reject_reason}); requeuing" ) - await task_queue.release_task(rejected_task_id, agent_id=agent_id) + released = await task_queue.release_task(rejected_task_id, agent_id=agent_id) + if released: + released_task = await task_queue.get_task(rejected_task_id) + released_run = await swarm_runtime.get_run_for_task(rejected_task_id) + if released_task and released_run: + await swarm_runtime.emit_event( + released_run, + "task.released", + task_id=rejected_task_id, + agent_instance_id=agent_id, + payload={ + "task_id": rejected_task_id, + "agent_role": released_task.agent_role, + "reason": reject_reason, + }, + ) elif message_type == "peer_message": # Route peer collaboration messages between agents; stamp the sender so the diff --git a/orchestrator/master_agent.py b/orchestrator/master_agent.py new file mode 100644 index 0000000..d577db9 --- /dev/null +++ b/orchestrator/master_agent.py @@ -0,0 +1,69 @@ +"""Master Agent — the decision-making entity of a swarm run. + +This makes the "master" a first-class entity rather than scattered orchestrator functions. +The master agent owns the *cognitive* loop: + - plan() : decompose the objective into specialist subtasks + - review_and_decide(): judge whether the combined result is good enough, and which tasks to redo + - synthesize() : compose the specialists' outputs into one user-facing answer + +It uses an LLM as its brain (via `planner`, with deterministic fallbacks). The orchestrator +remains the "hands": it executes the master's decisions (dispatch, reopen tasks, persist state, +emit events). This separation keeps decisions in one named entity while leaving runtime +mechanics (and the Manager contract) in the orchestrator. + +NOTE: agent→task dispatch is still capability-matched in the orchestrator loop; `select_agent` +below is the seam where the master can later own assignment (LLM-driven), without changing the +loop's contract today. +""" +from __future__ import annotations + +import logging +from typing import Any, Dict, List, Optional + +from .planner import planner + +logger = logging.getLogger(__name__) + + +class MasterAgent: + """The master agent: decompose → decide → synthesize for a swarm run.""" + + def __init__(self, brain=planner): + # `brain` is the LLM-backed planner (build_plan / review / synthesize), swappable for tests. + self.brain = brain + + async def plan(self, run_id: str, objective: str) -> List[Dict[str, Any]]: + """Decompose the user objective into specialist subtasks.""" + subtasks = await self.brain.build_plan(run_id, objective) + logger.info(f"[master] planned {len(subtasks)} subtask(s) for run {run_id}") + return subtasks + + async def review_and_decide(self, objective: str, tasks: List[Dict[str, Any]], + results: Dict[str, Any]) -> Dict[str, Any]: + """Decide whether the combined result is good enough. + + Returns the master's verdict: {accepted: bool, summary: str, retry_tasks: [task_id,...]}. + The orchestrator acts on it (reopen retry_tasks or finalize). + """ + verdict = await self.brain.review(objective, tasks, results) + logger.info( + f"[master] review decision: accepted={verdict.get('accepted')} " + f"retry={verdict.get('retry_tasks')}" + ) + return verdict + + async def synthesize(self, objective: str, results: Dict[str, Any]) -> str: + """Compose the specialists' outputs into one user-facing answer.""" + return await self.brain.synthesize(objective, results) + + def select_agent(self, task, candidates: List[Any]) -> Optional[Any]: + """Choose which agent runs a task. Seam for future LLM-driven assignment. + + Default: first capability-eligible candidate (capability matching is enforced upstream + by task_queue.get_ready_pending_task), preserving current dispatch behavior. + """ + return candidates[0] if candidates else None + + +# Singleton master agent for the runtime. +master_agent = MasterAgent() diff --git a/orchestrator/swarm_runtime.py b/orchestrator/swarm_runtime.py index c5182a6..20685ce 100644 --- a/orchestrator/swarm_runtime.py +++ b/orchestrator/swarm_runtime.py @@ -427,6 +427,15 @@ class SwarmRuntime: **redacted_payload, } + # Conform to HM event contract required fields (heicode agent_callback.go): + # timeline.updated requires `title`; budget.alert requires `threshold_pct`. + if event_type == "timeline.updated" and "title" not in redacted_payload: + redacted_payload["title"] = redacted_payload.get("summary") or "timeline" + if event_type == "budget.alert" and "threshold_pct" not in redacted_payload: + threshold = redacted_payload.get("threshold") + if isinstance(threshold, (int, float)): + redacted_payload["threshold_pct"] = threshold * 100 if threshold <= 1 else threshold + event_id = f"evt_{uuid.uuid4().hex}" body: Dict[str, Any] = { "event_id": event_id, diff --git a/scripts/test-baseline-comparison.py b/scripts/test-baseline-comparison.py new file mode 100644 index 0000000..fe46586 --- /dev/null +++ b/scripts/test-baseline-comparison.py @@ -0,0 +1,86 @@ +"""Test the baseline comparison adapter (benchmark/baselines/comparison.py) — v2.0. + +Run from agent_swarm_v6: python scripts/test-baseline-comparison.py +""" +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from benchmark.baselines import BenchmarkRunRecord, quality, unified_metrics, compare, evaluate + +failures = [] + + +def check(name, cond): + print(("PASS" if cond else "FAIL"), "-", name) + if not cond: + failures.append(name) + + +swarm = BenchmarkRunRecord( + system="swarm", scenario="coding", task_set_id="coding-set-1", n_agent=5, + completed_tasks=10, total_tasks=10, + test_pass_rate=90, code_review_score=80, user_acceptance=85, + budget_usd=10, actual_cost_usd=5, model_tokens=50000, + target_time_s=600, actual_time_s=500, recovered_failures=1, total_failures=1, +) +sub = BenchmarkRunRecord( + system="sub", scenario="coding", task_set_id="coding-set-1", n_agent=3, + completed_tasks=9, total_tasks=10, + test_pass_rate=70, code_review_score=60, user_acceptance=65, + budget_usd=10, actual_cost_usd=6, model_tokens=42000, + target_time_s=600, actual_time_s=720, recovered_failures=1, total_failures=2, +) + +# Quality: 0.4*90+0.3*80+0.3*85 = 85.5 ; 0.4*70+0.3*60+0.3*65 = 65.5 +check("Q_swarm = 85.5", quality(swarm) == 85.5) +check("Q_sub = 65.5", quality(sub) == 65.5) + +# DEFAULT is now symmetric C_base (Owner-ratified): C_base = n_base*(CostEff_base/100+0.5) = 3*(166.667/100+0.5) = 6.5 +r = compare(swarm, sub) +check("default mode is symmetric", r["cost_mode"] == "symmetric" and r["c_base"] == 6.5) +check("G_E = 20.0", r["g_e"] == 20.0) +check("C_swarm = 12.5 (5*(200/100+0.5))", r["c_swarm"] == 12.5) +check("G_E,c symmetric = (85.5/12.5)-(65.5/6.5)", round(r["g_e_cost"], 4) == round((85.5 / 12.5) - (65.5 / 6.5), 4)) +check("base_coefficient(sub) = 0.90", r["base_coefficient"] == 0.90) +check("raw gain positive", r["raw_gain_positive"] is True) +check("cost-normalized negative (5 agents not worth it here)", r["cost_normalized_positive"] is False) + +ev = evaluate(swarm, [sub]) +check("swarm_valid False (fails cost-normalized)", ev["swarm_valid"] is False) + +# Literal v2.0 (reference only): C_base = 1.0 +rl = compare(swarm, sub, symmetric_cost=False) +check("literal mode label + C_base=1.0", rl["cost_mode"] == "literal_v2.0" and rl["c_base"] == 1.0) +check("literal G_E,c = (85.5/12.5)-65.5", round(rl["g_e_cost"], 4) == round((85.5 / 12.5) - 65.5, 4)) + +um = unified_metrics(sub) +check("unified_metrics keys", set(um.keys()) == {"completion", "quality", "cost_efficiency", "speed", "robustness"}) +check("unified completion 90", um["completion"] == 90.0) + +# from_dict validation +ok = BenchmarkRunRecord.from_dict({f: getattr(sub, f) for f in BenchmarkRunRecord.__dataclass_fields__}) +check("from_dict round-trips", ok == sub) +try: + BenchmarkRunRecord.from_dict({"system": "sub"}) + check("from_dict rejects missing fields", False) +except ValueError: + check("from_dict rejects missing fields", True) +try: + compare(sub, swarm) # first arg must be system='swarm' + check("compare requires swarm record first", False) +except ValueError: + check("compare requires swarm record first", True) +try: + bad = BenchmarkRunRecord.from_dict({f: getattr(sub, f) for f in BenchmarkRunRecord.__dataclass_fields__} | {"task_set_id": "other"}) + compare(swarm, bad) + check("compare requires same task_set_id", False) +except ValueError: + check("compare requires same task_set_id", True) + +print() +if failures: + print(f"{len(failures)} comparison check(s) FAILED: {failures}") + sys.exit(1) +print("all baseline comparison checks passed (v2.0)") diff --git a/scripts/test-benchmark-collector.py b/scripts/test-benchmark-collector.py new file mode 100644 index 0000000..d6cfbe8 --- /dev/null +++ b/scripts/test-benchmark-collector.py @@ -0,0 +1,95 @@ +"""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"}) + + 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) + # 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_communication", + "s_swarm", "g_e", "g_e_cost", "benchmark"))) + + 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()) diff --git a/scripts/test-benchmark-metrics.py b/scripts/test-benchmark-metrics.py new file mode 100644 index 0000000..23f6602 --- /dev/null +++ b/scripts/test-benchmark-metrics.py @@ -0,0 +1,72 @@ +"""Unit tests for benchmark metric formulas — Agent 蜂群指标量化与标准 v2.0. + +Run from agent_swarm_v6: python scripts/test-benchmark-metrics.py +""" +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +import benchmark.metrics as m + +failures = [] + + +def check(name, cond): + print(("PASS" if cond else "FAIL"), "-", name) + if not cond: + failures.append(name) + + +# --- weight tables sum/shape (v2.0) --- +check("Σλ = 1.0", abs(sum(m.LAMBDA_WEIGHTS.values()) - 1.0) < 1e-9) +check("τ weights sum = 1.0", abs(sum(m.TAU_WEIGHTS.values()) - 1.0) < 1e-9) +check("η weights sum = 1.0", abs(sum(m.ETA_WEIGHTS.values()) - 1.0) < 1e-9) +check("reward weights sum = 1.0", abs(sum(m.REWARD_WEIGHTS.values()) - 1.0) < 1e-9) +check("S_swarm weights sum = 1.0", abs(sum(m.SWARM_WEIGHTS.values()) - 1.0) < 1e-9) +check("η includes Confidence (v2.0)", "confidence" in m.ETA_WEIGHTS) + +# --- decision layer --- +# pheromone with all=1 except penalties=0 -> sum of positive weights 0.25+0.20+0.20 = 0.65 +check("pheromone positive-only", round(m.pheromone(success=1, quality=1, acceptance=1, cost=0, time=0, risk=0, rollback=0), 4) == 0.65) +check("heuristic positive-only", round(m.heuristic(match=1, urgency=1, dependency=1, resource=1, confidence=1, risk=0, budget_pressure=0), 4) == 0.80) +# p_decision = τ^α·η^β·100 ; τ=0.5, η=0.5, α=1, β=2 -> 0.5*0.25*100 = 12.5 +check("p_decision = τ^α·η^β·100", round(m.p_decision(0.5, 0.5, 1.0, 2.0), 4) == 12.5) +# action_probability normalizes +ap = m.action_probability(2.0, 2.0, 1.0, 1.0, [(2.0, 2.0), (1.0, 1.0)]) +check("action_probability normalized", round(ap, 4) == round(4.0 / 5.0, 4)) + +# --- execution layer --- +# reward with all components=100, penalties=0 -> sum of positive weights *100 = (0.20+0.20+0.12+0.13+0.13+0.10)*100 = 88 +check("reward positive-only = 88.0", + round(m.reward(s_task=100, q_quality=100, v_speed=100, e_cost=100, r_robust=100, g_gov=100, p_risk=0, p_rework=0), 4) == 88.0) +check("quality_score", m.quality_score(100, 100, 100) == 100.0) + +# --- emergence + cost-normalized (v2.0 CHANGED to difference + dynamic C_swarm) --- +check("emergence_gain", m.emergence_gain(90, 80) == 10) +# C_swarm = N_agent*(CostEfficiency/100 + 0.5); N=4, CE=50 -> 4*(0.5+0.5)=4.0 +check("swarm_cost dynamic", m.swarm_cost(4, 50) == 4.0) +# G_E,c = (Q_swarm/C_swarm) - (Q_base/C_base); (80/4)-(80/1)... use (80/4)-(20/1)=20-20=0 +check("cost_normalized_gain is a difference", m.cost_normalized_gain(80, 4.0, 20, 1.0) == 0.0) + +# --- benchmark_agent: Σλ enforcement --- +check("benchmark_agent uses default Σλ=1 weights", + round(m.benchmark_agent(s_swarm=10, g_e=0, reward=0, observability=0, governance=0), 4) == round(0.30 * 10, 4)) +try: + m.benchmark_agent(s_swarm=1, g_e=1, reward=1, observability=1, governance=1, + weights={"lambda1": 0.5, "lambda2": 0.5, "lambda3": 0.5, "lambda4": 0.5, "lambda5": 0.5}) + check("benchmark_agent rejects Σλ != 1.0", False) +except ValueError: + check("benchmark_agent rejects Σλ != 1.0", True) + +# --- dataclass shape (v2.0) --- +expected_fields = {"tau", "eta", "p_decision", "reward", "s_completion", "s_gain", "s_collaboration", + "s_communication", "s_cost", "s_robustness", "s_governance", "s_swarm", + "g_e", "g_e_cost", "benchmark"} +check("SwarmMetrics has v2.0 fields", set(m.SwarmMetrics.__dataclass_fields__.keys()) == expected_fields) + +print() +if failures: + print(f"{len(failures)} formula check(s) FAILED: {failures}") + sys.exit(1) +print("all benchmark metric formula checks passed (v2.0)") diff --git a/scripts/test-contract-events.py b/scripts/test-contract-events.py new file mode 100644 index 0000000..41e2a64 --- /dev/null +++ b/scripts/test-contract-events.py @@ -0,0 +1,137 @@ +"""Manager event-contract test. + +Validates that Swarm's emitted callback events conform to the Heicode Manager handler +(`heicode/controller/agent_callback.go`): +- each event_type carries HM's required payload fields, +- the HMAC signing canonical string / headers match HM's verification. + +Hermetic: REDIS_FAKE in-memory store; callbacks are captured in-process (not sent). +Run from agent_swarm_v6: python scripts/test-contract-events.py +""" +import asyncio +import hashlib +import hmac +import json +import os +import sys +from pathlib import Path + +os.environ["REDIS_FAKE"] = "1" +os.environ["AGENT_CALLBACK_SIGNING_SECRET"] = "test-signing-secret" +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 + +# HM required payload fields per event_type (agent_callback.go registry). +REQUIRED = { + "deployment.status_changed": ["status"], + "task.created": ["task_id", "title"], + "task.claimed": ["task_id", "agent_role"], + "task.running": ["task_id", "agent_role"], + "task.heartbeat": ["task_id", "agent_role"], + "task.blocked": ["task_id", "reason"], + "task.retried": ["task_id", "attempt"], + "task.released": ["task_id", "agent_role"], + "task.failed": ["task_id", "reason"], + "task.completed": ["task_id"], + "handoff.requested": ["task_id", "from_role", "to_role"], + "handoff.completed": ["task_id", "from_role", "to_role"], + "approval.requested": ["approval_id", "operation", "risk_level"], + "artifact.created": ["artifact_id"], + "timeline.updated": ["title"], + "budget.alert": ["threshold_pct"], +} + +SECRET = os.environ["AGENT_CALLBACK_SIGNING_SECRET"] +captured = [] # (raw_body, headers) +failures = [] + + +def check(name, cond): + print(("PASS" if cond else "FAIL"), "-", name) + if not cond: + failures.append(name) + + +def validate_envelope(raw_body: str, headers: dict) -> None: + env = json.loads(raw_body) + et = env["event_type"] + # required fields + req = REQUIRED.get(et, []) + payload = env.get("payload") or {} + missing = [f for f in req if f not in payload or payload.get(f) in (None, "")] + check(f"{et}: required fields present {req}", not missing) + # HMAC canonical string == ts + "." + event_id + "." + raw_body + ts = headers.get("X-Agent-Timestamp") + sig = headers.get("X-Agent-Signature", "") + eid = headers.get("X-Agent-Event-Id") + expect = "sha256=" + hmac.new(SECRET.encode(), f"{ts}.{eid}.{raw_body}".encode(), hashlib.sha256).hexdigest() + check(f"{et}: HMAC signature matches HM canonical string", sig == expect) + check(f"{et}: event_id header == body event_id", eid == env.get("event_id")) + + +async def _capture(self, swarm_id, url, raw_body, headers, event_type, event_id): + captured.append((raw_body, headers)) + + +async def emit(run, event_type, **payload): + captured.clear() + await swarm_runtime.emit_event(run, event_type, payload=payload) + await asyncio.sleep(0) # let the create_task'd callback run + assert captured, f"no callback captured for {event_type}" + return captured[-1] + + +async def main(): + await redis_client.connect() + sr_mod.SwarmRuntime._post_callback = _capture # capture instead of HTTP POST + + body = { + "mode": "swarm", + "requirement": {"objective": "contract test"}, + "callback": {"url": "http://127.0.0.1:9/cb", "subscribed_events": []}, + "metadata": {"manager_deployment_id": "m-contract"}, + } + run, _ = await swarm_runtime.get_or_create_run(body=body, idempotency_key=None, correlation_id="corr-contract") + + # 1) centralized normalization (the 2 emit_event fixes) + raw, hdr = await emit(run, "timeline.updated", summary="did the thing") + check("timeline.updated normalized to include title", json.loads(raw)["payload"].get("title") == "did the thing") + validate_envelope(raw, hdr) + + raw, hdr = await emit(run, "budget.alert", threshold=0.8) + check("budget.alert normalized to threshold_pct=80", json.loads(raw)["payload"].get("threshold_pct") == 80.0) + validate_envelope(raw, hdr) + + # 2) the handler-built payloads for the other two fixed types + raw, hdr = await emit(run, "handoff.completed", task_id="t1", from_role="implementation", to_role="testing") + validate_envelope(raw, hdr) + raw, hdr = await emit(run, "task.released", task_id="t1", agent_role="testing", reason="at_capacity") + validate_envelope(raw, hdr) + + # 3) representative coverage of the common event types + samples = { + "deployment.status_changed": {"status": "running"}, + "task.created": {"task_id": "t1", "title": "impl"}, + "task.claimed": {"task_id": "t1", "agent_role": "implementation"}, + "task.completed": {"task_id": "t1"}, + "task.failed": {"task_id": "t1", "reason": "boom"}, + "task.retried": {"task_id": "t1", "attempt": 1}, + "approval.requested": {"approval_id": "ap1", "operation": "git.write", "risk_level": "high"}, + "artifact.created": {"artifact_id": "art1"}, + } + for et, pl in samples.items(): + raw, hdr = await emit(run, et, **pl) + validate_envelope(raw, hdr) + + print() + if failures: + print(f"{len(failures)} contract check(s) FAILED: {failures}") + sys.exit(1) + print("all event-contract checks passed") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scripts/test-merge-smoke.py b/scripts/test-merge-smoke.py index 0579eaf..5784d19 100644 --- a/scripts/test-merge-smoke.py +++ b/scripts/test-merge-smoke.py @@ -113,10 +113,11 @@ async def test_agent_peer_routing(): "type": "peer_message", "from_agent_id": "agent-impl", "task_id": "t1", - "content": "please advise", + "content": "", # empty -> deterministic fallback reply (no model needed) "correlation_id": "corr-1", "is_reply": False, }) + await asyncio.sleep(0.05) # answer_peer_query is scheduled as a background task check("peer query produces a reply", len(sent) == 1 and sent[0]["is_reply"] is True) check("peer reply targets requester", sent[0]["target_agent_id"] == "agent-impl") @@ -239,8 +240,26 @@ async def test_agent_peer_shares_summary(): sent.append(payload) a.safe_send = fake_send + + # Fallback path: no query content -> cached summary (no model needed). await a.answer_peer_query({"from_agent_id": "agent-test", "correlation_id": "c", "is_reply": False}) - check("peer reply shares last summary", "implemented add()" in sent[0]["content"]) + check("peer reply falls back to last summary", "implemented add()" in sent[0]["content"]) + + # Substantive path: a stubbed executor returns a grounded, structured reply. + class _FakeExec: + async def peer_reply(self, *, query, capabilities, last_summary, max_tokens=None): + return {"content": f"grounded answer to: {query}", "stance": "agree", + "evidence": "src", "refs": ["a.py"]} + + a._peer_executor = _FakeExec() + sent.clear() + await a.answer_peer_query({ + "from_agent_id": "agent-test", "correlation_id": "c2", "task_id": "t", + "content": "does add() raise ValueError?", "is_reply": False, + }) + check("substantive peer reply uses executor content", + sent[0]["content"] == "grounded answer to: does add() raise ValueError?" + and sent[0]["stance"] == "agree" and sent[0]["refs"] == ["a.py"]) async def main():