复审整改(PR #26):删死开关 helper、文档对齐"无条件"、swarm.health 改内部、#6 降为 Refs

回应 Fasthei 终审三点:

1) [P1 文档/代码冲突 + 死代码] 删除从不被调用的 *_enabled() helper(autonomous_tasks.proposals_enabled
   / task_competition.task_competition_enabled / convergence.convergence_report_enabled)及其
   import os;模块 docstring 与四份协议文档(autonomous-task-generation / task-competition-protocol
   / review-loop-protocol / convergence-protocol)从"默认关/未接入/待 PR/cutover 转无条件"全部改为
   "无条件接入(无开关)",删除引用死 helper 的过时集成代码样例;同步删除三个模块单测里的
   "flag default OFF" 断言。

2) [P1 验收] #6 "Closes" 降为 "Refs":#6 DoD 需 ARB 决策记录链接,当前只有 owner 指示断言、无链接。
   product-positioning.md 改为如实记录决策来源(owner 指示 + 本 PR + 文档)并把"补 ARB 记录链接(或
   owner 明确接受断言)"列为关闭 #6 的前置;纠正其"flag 门控、默认行为不变"的过时表述(重构已无条件)。

3) [P2 契约卫生] assess_swarm_health 不再 emit_event("swarm.health")(避免向订阅全部的 Manager 回调
   投递未注册事件);改为存 run.metadata["health"] + 内部 health_log。test-swarm-guard 相应断言
   "无 swarm.health 外发 + 内部 health_log 已记"。

本地受影响 11 套全绿。影响范围:agent_swarm(orchestrator 模块/文档/测试);不改 Manager↔Swarm 契约。

Refs #6
Refs #7
Refs #8
Refs #11
Refs #12
Refs #18

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Songhaoz666
2026-06-10 16:48:27 +08:00
co-authored by Claude Opus 4.8
parent f281370209
commit f2662e4268
13 changed files with 69 additions and 292 deletions
+5 -2
View File
@@ -1,6 +1,8 @@
# 产品定位说明(Product Positioning)
> 状态:**已选定 Path B 并启动去中心化重构(owner 指示,分阶段进行)**。本文回应工单 #6「蜂群模式产品定义偏差」:当前实现仍为 **Master 中心化**,heicodeDocs 蜂群定义为**弱中心**;现按 **Path B** 推进到弱中心自组织(计划见 [swarm/decentralized-rework-plan.md](./swarm/decentralized-rework-plan.md))。重构全程 flag 门控、默认行为不变,直至各阶段验收后再切默认。**形式 ARB 记录待补**;两条路径取舍保留如下以备追溯。
> 状态:**已选定 Path B 并完成去中心化重构(owner 指示)**。本文回应工单 #6「蜂群模式产品定义偏差」:原实现为 **Master 中心化**,heicodeDocs 蜂群定义为**弱中心**;现已按 **Path B** 重构为弱中心自组织并设为**唯一行为**(无开关;计划见 [swarm/decentralized-rework-plan.md](./swarm/decentralized-rework-plan.md))。
>
> **决策记录与 #6 关闭口径**:路径选择的依据 = owner 在工单/会话中的指示 + 本重构 PR + 本文 + [swarm-definition-gap.md](./swarm-definition-gap.md)。但**尚无形式 ARB 决策记录的链接**。故重构 PR 以 **`Refs #6`**(不自动关闭)引用本单;待补 ARB 记录链接(或 owner 明确接受上述断言为准)后由 owner 关闭 #6。两条路径取舍保留如下以备追溯。
>
> 配套:[`swarm-definition-gap.md`](./swarm-definition-gap.md)(逐条差距与文件/行号引用)、[README.md](../README.md)(能力边界表)、[`integration/runtime-contract.md`](./integration/runtime-contract.md)、[`benchmark/IMPORTANT-metric-coverage-gaps.md`](./benchmark/IMPORTANT-metric-coverage-gaps.md)。
@@ -32,7 +34,8 @@
## 4. 待裁定项(ARB 决策,非本仓自决)
- [x] **已选定 Path B(owner 指示,分阶段重构)** —— 见 `swarm/decentralized-rework-plan.md`。形式 ARB 记录待补;仍可在任一阶段验收门回退(重构 flag 门控、默认未启用)。
- [x] **已选定 Path B(owner 指示)并完成重构** —— 见 `swarm/decentralized-rework-plan.md`(swarm 已是唯一行为,无开关)。
- [ ] **关闭 #6 前置**:补一个**形式 ARB 决策记录链接**(或 owner 明确接受「owner 指示 + 本 PR + 本文」为决策记录)。在此之前重构 PR 用 `Refs #6` 不自动关闭。
- [ ] 若选 Path A:确认收敛后的对外定位口径与 heicodeDocs 标准无冲突(如有冲突,按 CLAUDE.md 输出冲突点交标准源裁定)。
- [ ] 若选 Path B:确认 #7/#8/#9/#11/#12 的范围、依赖顺序与 Benchmark 验收门(基线对比,见 `benchmark/IMPORTANT-metric-coverage-gaps.md` §5)。
+7 -83
View File
@@ -11,8 +11,8 @@ Issue #7:执行单元(Agent)在工作中会**发现**当前共享状态缺
**关键边界(务必先读)**:
- **是 Agent 在提案,不是 Master 在生成任务。** Master/planner 的自顶向下分解走的是既有路径(`master_agent.plan` + `ENABLE_PLANNER_FALLBACK`),与本机制**互不混用**。本路径产出的提案恒带 `source="agent_proposed"` 与 `proposed_by_agent_id`;`assert_not_master_origin()` 在运行时拒绝任何 proposer/trigger 名含 `master`/`planner` 的提案,防止 Master 借此路径「洗白」任务来源、绕过 planner 契约。
- **纯机制、默认关闭。** 整个能力由环境开关 `ENABLE_AGENT_TASK_PROPOSALS`(默认 `false`)门控;关闭时运行时行为与改动前逐字节一致(与 `ENABLE_PLANNER_FALLBACK` / `ENABLE_REVIEW_LOOP` / `ENABLE_SUBTASK_HANDOFF` 同一约定)。
- **是 Agent 在提案,不是 Master 在生成任务。** Master/planner 的自顶向下分解路径**已在去中心化重构中删除**;本机制(自底向上提案)是蜂群**唯一**的分解路径。提案恒带 `source="agent_proposed"` 与 `proposed_by_agent_id`;`assert_not_master_origin()` 在运行时拒绝任何 proposer/trigger 名含 `master`/`planner` 的提案,防止 Master 借此路径「洗白」任务来源。
- **无条件接入(无开关)。** 该机制是蜂群运行时的固有行为,**无 `ENABLE_*` 开关**(本仓即 swarm 运行时,模式选择在仓外);`main.py` 的 WS `task_proposal` 分支与 `handle_task_proposal` 始终生效。
- **Agent 只提案,编排器才决策。** Agent 给出自评置信度 `proposal_confidence`,但「收 / 拒 / 并」由编排器侧的策略函数 `review_proposal` 依据置信度阈值、提案预算、与现存任务去重来裁决——不调用任何模型。
## 2. 数据模型(`orchestrator/autonomous_tasks.py`)
@@ -69,91 +69,15 @@ proposed ──accept──▶ accepted ──ingest──▶ (新建 PENDING
事件类型常量:`EVENT_SUBMITTED` / `EVENT_ACCEPTED` / `EVENT_REJECTED` / `EVENT_MERGED`。
## 4. 已实现 vs. 未集成(诚实声明,规则 #9)
## 4. 接入现状(已无条件接入)
**已实现且测试覆盖**:`TaskProposal` / lineage / 状态枚举、`review_proposal` 三路裁决(置信度 / 预算 / 去重)、`ingest_accepted_proposal` 的 spec 映射与 lineage 透传、生命周期事件构建、`assert_not_master_origin` 守卫、`ENABLE_AGENT_TASK_PROPOSALS` 开关读取。
**已落地于 `orchestrator/main.py`**(去中心化重构,无开关):WS `task_proposal` 分支 → `handle_task_proposal`:构造 `TaskProposal` → `review_proposal`(置信度/去重合并/每 run 预算 `AGENT_PROPOSAL_BUDGET`)→ ACCEPT 即 `task_queue.create_task` 入队为真实 PENDING 任务(带 lineage、`source=agent_proposed`、发标准 `task.created`);提案生命周期存 `run.metadata["proposals"]`(**内部遥测,非 Manager 事件**)。这是蜂群唯一的分解路径。集成测试 `scripts/test-swarm-autonomous.py`。
**尚未集成(需集成方按 §5 手工接入)**:
模块本身(`autonomous_tasks.py`)仍是纯策略/数据层(无 Redis/WS/模型):`TaskProposal`/lineage/状态枚举、`review_proposal` 三路裁决、`ingest_accepted_proposal` 映射、生命周期事件、`assert_not_master_origin` 守卫。
- 未接入 `orchestrator/main.py` 的实时 WS 主循环——目前没有 `task_proposal` 入站分支,Agent 端也未发送该消息。
- 未接入派发器——ACCEPTED 提案产出的 spec 尚未被任何代码实际 `create_task` 入队。
- `TaskProposal` / `SwarmRun` 的持久化字段未添加(本次按硬约束只新建文件,不改共享文件)。
- `expired` 状态的过期判定(按预算/时限)逻辑未实现,仅定义了枚举。
- 提案事件**不是**已注册的 Manager 事件,按内部遥测处理(与 `peer_message`、ACO `decisions` 同策略),集成方需决定映射为既有 HM 事件还是仅作 telemetry。
**仍为后续**:`expired` 过期判定仅定义枚举未实现;`Task`/`SwarmRun` 未加持久化提案字段(提案存于 `run.metadata`);真实 Agent 执行器基于 LLM 决定「提哪些子任务」为后续(`scripts/stub_agent.py` 已演示种子→提案)。
## 5. 集成说明(Integration notes)
以下改动需由集成方应用到**共享文件**(本次交付未修改这些文件):
### 5.1 新增 WS `message_type`:`task_proposal`(`orchestrator/main.py`)
在 `websocket_endpoint` 的消息分支中(与 `peer_message` / `handoff_request` 同级)新增:
```python
elif message_type == "task_proposal":
# 默认关闭:开关未开则直接丢弃,运行时行为不变。
if not proposals_enabled():
continue
proposal = TaskProposal(
proposed_by_agent_id=agent_id,
title=message.get("title"),
description=message.get("description", ""),
proposal_reason=message.get("proposal_reason", ""),
proposal_confidence=message.get("proposal_confidence", 0.0),
agent_role=message.get("agent_role", "general"),
required_capabilities=message.get("required_capabilities") or [],
depends_on=message.get("depends_on") or [],
lineage=ProposalLineage(
origin_task_id=message.get("origin_task_id"),
trigger_event=message.get("trigger_event"),
shared_state_snapshot=message.get("shared_state_snapshot") or {},
),
)
run = await swarm_runtime.get_run_for_task(message.get("origin_task_id")) \
if message.get("origin_task_id") else None
# 用 run 任务图构造 ExistingTaskRef 列表做去重
existing = [
ExistingTaskRef(t.task_id, t.description, t.agent_role, t.status.value)
for t in await load_runtime_tasks(run)
] if run else []
policy = ProposalPolicy(
remaining_proposal_budget=int((run.metadata.get("proposal_budget", 3)) if run else 3),
)
outcome = review_proposal(proposal, policy, existing_tasks=existing)
if run:
for ev in build_lifecycle_events_for_outcome(proposal, outcome):
await swarm_runtime.emit_event(run, ev["event_type"], task_id=proposal.lineage.origin_task_id,
agent_instance_id=agent_id, payload=ev)
if outcome.decision == ProposalDecision.ACCEPT and run:
spec = ingest_accepted_proposal(proposal, swarm_id=run.swarm_id)
new_task = await task_queue.create_task(
task_id=f"{run.swarm_id}-{proposal.proposal_id}",
title=spec["title"], description=spec["description"],
agent_role=spec["agent_role"], required_capabilities=spec["required_capabilities"],
depends_on=spec["depends_on"], parent_task_id=spec["parent_task_id"],
root_task_id=spec["root_task_id"] or f"{run.swarm_id}-{proposal.proposal_id}",
source=spec["source"], context={**spec["context"], "swarm_id": run.swarm_id},
max_retries=3,
)
await swarm_runtime.attach_task(run, new_task.task_id)
run.metadata["proposal_budget"] = max(0, policy.remaining_proposal_budget - 1)
await swarm_runtime.save_run(run)
await websocket.send_json({"type": "task_proposal_ack",
"proposal_id": proposal.proposal_id,
"decision": outcome.decision.value})
```
并在 `main.py` 顶部新增 import:
```python
from .autonomous_tasks import (
proposals_enabled, TaskProposal, ProposalLineage, ProposalPolicy,
ProposalDecision, ExistingTaskRef, review_proposal, ingest_accepted_proposal,
build_lifecycle_events_for_outcome,
)
```
### 5.2 Agent 端发送(`agent/main.py`)
## 5. Agent 端发送(`agent/main.py`)
新增一个方法(与 `request_handoff` 同风格),由执行器在发现缺口时调用:
+11 -76
View File
@@ -1,6 +1,6 @@
# 蜂群收敛协议(共识 / 冲突消解 / 终止函数)
> 状态:**纯函数层已落地并通过单测;已接入 `refresh_swarm_run_status`(shadow 模式,去中心化重构 P1)**。feature flag `ENABLE_CONVERGENCE_REPORT`(构建期;cutover 转无条件):每次 run 终态产 `ConvergenceReport` 存 `run.metadata["convergence"]`,并把 `termination_reason` 附到 `timeline.updated`;**shadow——暂不覆盖 `run.status`**(cutover 转 authoritative)。
> 状态:**已无条件接入 `refresh_swarm_run_status`(去中心化重构,无开关)**。每次 run 终态都产 `ConvergenceReport` 存 `run.metadata["convergence"]`,并把 `termination_reason` 附到 `timeline.updated`。**当前为解释性**——产出 `termination_reason`/共识/冲突,但**不覆盖 `run.status`**(今天 next_status 与报告对 completed/failed 一致);authoritative 状态覆盖为后续。
>
> 对应 Issue #12:「蜂群收敛机制缺失:缺少共识、冲突消解与终止函数,无法解释 Swarm 为什么结束」。
>
@@ -19,12 +19,12 @@
- **一等公民的冲突检测 / 消解**:两个 Agent 对同一文件写出不同内容、依赖未满足却标记完成等,均无显式识别;
- **可机读的终止原因**:`completed` / `failed` 无法区分「质量达标」「预算耗尽」「轮次上限」「被风险阻断」。
本协议补齐上述三者,且**严格作为旁路只读层**:不改变任何 run 的现有终态语义(规则 #9:如实标注未接入)。
本协议补齐上述三者。**已无条件接入**(无开关),但当前**作为解释层**:产出报告 + `termination_reason` 存 run,**不覆盖** run 的终态语义(规则 #9:authoritative 覆盖为后续)。
## 1. 设计原则
- **纯函数、可测**:`evaluate_convergence(run_state)` 不做任何 I/O,不修改 run;调用方负责持久化与发事件。
- **默认关闭**:`ENABLE_CONVERGENCE_REPORT`(默认 `false`),与 `ENABLE_REVIEW_LOOP` / `ENABLE_ACO_DISPATCH` 同构。关闭时整套逻辑不执行,Manager 契约与完成语义逐字节不变。
- **纯函数、可测**:`evaluate_convergence(run_state)` 不做任何 I/O,不修改 run;调用方(`main.py: compute_convergence_report`)负责持久化。
- **无条件(无开关)**:`refresh_swarm_run_status` 每次终态都计算报告——本仓即 swarm 运行时,无 `ENABLE_*` 门控。
- **不伪造信号**(规则 #9):无质量评分 ≠ 通过;无测试信号 ≠ 失败;无预算 ≠ 充足。缺信号一律退出判定,不补 0、不补 100。
- **解释优先于干预**:本版**只解释**「为什么结束」,不自动改写终态、不自动 merge 冲突产物。
@@ -118,77 +118,12 @@ consensus = 100 × (未被任何冲突牵连的已完成任务数 / 已完成任
模块级、无 WS/Redis/模型。覆盖:两个冲突产物 → 检出 artifact_mismatch + review_disagreement;review_disagreement 可消解、artifact_mismatch 未消解;产出带具体 `termination_reason` 的 `ConvergenceReport`;**断言每个终态都带 termination_reason**;断言至少一个冲突「检出 + 消解」闭环;以及五个终止原因各自的终态用例。全部 PASS。
## 6. 诚实差距(规则 #9)
## 6. 接入现状与诚实差距(规则 #9)
- **未接入**:`evaluate_convergence` 当前**不被 `refresh_swarm_run_status` 调用**,不改变任何 run 的真实终态。现状仍是「任务做完 + 评审通过」,本协议为旁路只读层。
- **质量/预算/风险输入有条件**:`quality` 依赖 Group B fixture 评分(仅 `ENABLE_QUALITY_EVAL` + 绑定 fixture 时存在);`budget`/`usage` 需 run 提供限额与用量;`risks` 需上游显式注入阻断风险。缺失时相应原因不触发,回退 `tasks_completed`。
- **消解为保守首版**:不做自动 merge / 自动选胜,硬冲突一律留待重做或人工。
- **事件未登记**:六个收敛事件尚未进入 Manager 事件注册表(见 §4 警告与下方集成说明)。
**已接入**(`main.py: compute_convergence_report` + `refresh_swarm_run_status`,无开关):每次 run 终态装配 `run_state`(tasks/budget/usage/quality/review_cycles)→ `evaluate_convergence` → 报告存 `run.metadata["convergence"]`,`termination_reason` 附到 `timeline.updated` 载荷。
## 集成说明(Integration notes)
以下说明**接入时**应如何改 `refresh_swarm_run_status` 与相关契约。**本任务不修改任何既有文件**,仅在此声明落点,供后续 PR 实施。
### 计算落点
在 `orchestrator/main.py:refresh_swarm_run_status` 中,**在已算出 `next_status`(行 ~421)之后、`run.status = next_status`(行 ~433)之前**插入(伪代码):
```python
from .convergence import (
convergence_report_enabled, evaluate_convergence,
event_convergence_started, event_conflict_detected, event_conflict_resolved,
event_consensus_updated, event_convergence_reached, event_convergence_failed,
)
report = None
if convergence_report_enabled():
run_state = {
"tasks": tasks, # 已在函数内拉取
"review_verdict": run.metadata.get("review_verdict"), # 需 maybe_run_review_cycle 顺带存入 metadata
"budget": (run.request_body.get("orchestration_plan") or {}).get("budget") or {},
"usage": run.metadata.get("usage_totals") or {}, # 由 emit_usage_event 累加得到
"quality": run.quality or {}, # Group B fixture 评分
"risks": run.metadata.get("risks") or [],
"review_cycles": int(run.metadata.get("review_cycles", 0) or 0),
"max_review_cycles": review_max_cycles(),
}
report = evaluate_convergence(run_state)
```
`evaluate_convergence` 是纯函数,**不替代** `next_status`;接入策略二选一:
- **影子模式(建议先行)**:只把 `report` 写入 `run.metadata["convergence"]` 并发事件,**不改 `run.status`**。用于在生产观察一段时间,验证收敛判定与现有终态一致后再升级。
- **权威模式(后续)**:让 `report.status` 参与/覆盖 `next_status`(如 `report.status == BLOCKED` 时把 run 置 `blocked`)。此举改变 Manager 面终态语义,**必须先过 `scripts/test-runtime-contract.py` 并按契约评审**。
### 新增事件
在计算 `report` 前后按需发:
```python
if report is not None:
await swarm_runtime.emit_event(run, *event_convergence_started(run_state))
for c in report.conflicts:
await swarm_runtime.emit_event(run, *event_conflict_detected(c))
for c in report.resolved_conflicts:
await swarm_runtime.emit_event(run, *event_conflict_resolved(c))
await swarm_runtime.emit_event(run, *event_consensus_updated(report))
if report.status == ConvergenceStatus.CONVERGED:
await swarm_runtime.emit_event(run, *event_convergence_reached(report))
elif report.is_terminal():
await swarm_runtime.emit_event(run, *event_convergence_failed(report))
```
**前置契约工作**:六个 `event_type`(`convergence.started` / `conflict.detected` / `conflict.resolved` / `consensus.updated` / `convergence.reached` / `convergence.failed`)须先在 Manager 侧 `agent_callback.go` 事件注册表登记必填字段;未登记前 `callback.subscribed_events` 不会投递,且不得在 `docs/integration/` 契约外私自消费。`emit_event` 已对 `timeline.updated`/`budget.alert` 做必填字段补齐,新增事件若有必填字段须比照补齐。
### 在终态附加 termination_reason
接入后,给终态的 `deployment.status_changed` 载荷补一个 `termination_reason`,让 Manager / 客户端能解释「为什么结束」。`status_payload` 不在本任务修改范围内,接入 PR 可在调用处合并:
```python
status_payload = swarm_runtime.status_payload(run, phase=..., deliverable=deliverable)
if report is not None and report.termination_reason is not None:
status_payload["termination_reason"] = report.termination_reason.value
await swarm_runtime.emit_event(run, "deployment.status_changed", payload=status_payload)
```
> 该字段为**新增可选字段**,对旧消费方向后兼容;纳入契约前应在 `docs/integration/` 标注,并在 PR 影响范围声明影响 Manager / agent_swarm / 审计链路。
**诚实差距**:
- **解释性,非权威**:报告**不覆盖** `run.status`(今天 next_status 与报告对 completed/failed 一致)。authoritative 模式(如 `BLOCKED`→置 run `blocked`)改变 Manager 面终态语义,须先过 `scripts/test-runtime-contract.py` + 契约评审——列为后续。
- **质量/预算/风险输入有条件**:`quality` 依赖 Group B fixture 评分(绑定 fixture 时);`budget`/`usage` 需 run 提供;`risks` 需上游注入。缺失时相应原因不触发,回退 `tasks_completed`(不伪造)。
- **消解为保守首版**:不做自动 merge / 自动选胜,硬冲突留待重做或人工。
- **收敛事件不进 Manager 流**:六个 `convergence.*`/`conflict.*`/`consensus.*` 事件**构建器已实现但不经 `emit_event` 外发**(未在 Manager `agent_callback.go` 注册;与 `swarm.health` 同策略,避免向订阅全部的回调投递未登记事件)。登记后方可启用 Manager 侧发送。`termination_reason` 以**新增可选字段**附在 `timeline.updated`,对旧消费方向后兼容。
+7 -27
View File
@@ -68,38 +68,18 @@
- **分歧偏置**:当 `disagreement=True` 时,`collaboration` 既 +1 票**又赢平票**——评审者分裂本身就是跨专家一致性缺口的证据;
- 全无信号 → `unknown`(不伪造)。
## 3. 集成说明(Integration notes)
## 3. 接入现状(已无条件接入,取代单评审)
> 本模块**当前未接入在线 finalize 路径**(诚信规则 #9)。`orchestrator/main.py` 的 `maybe_run_review_cycle` 仍是单评审主控门。以下为**预期接入方式**,接入前不改变任何现有行为与 Manager 契约。
> 同伴交叉评审是蜂群**唯一**的评审路径,**无开关**:去中心化重构已**删除**单 critic 主控评审环(`maybe_run_review_cycle` / `review_loop_enabled`)。`main.py: run_cross_review` 在 `refresh_swarm_run_status` 中无条件运行(<2 评审时为 no-op,放行收敛)。
### 3.1 开关(默认关闭)
### 3.1 评审者来源(≥ 2 独立意见)
新增功能开关 **`ENABLE_CROSS_REVIEW`(默认关)**,与现有 `ENABLE_REVIEW_LOOP` 正交:
同伴 Agent 经 WS `review_decision` 消息提交独立评审 → `main.py: handle_review_decision` 累积到 `run.metadata["reviews"]`;`run_cross_review` 在收齐 ≥ 2 条时 `aggregate_reviews(...)` 仲裁。`reviewer_agent_id` 取提交者,`weight` 可按角色信任赋值。`weighted` 法用 `weight·confidence` 比较,平票安全偏向拒绝。
- `ENABLE_CROSS_REVIEW` 未开:完全保持今天的单评审 Supervisor Retry 行为;
- `ENABLE_CROSS_REVIEW=1`(且 `ENABLE_REVIEW_LOOP=1`):`maybe_run_review_cycle` 改为收集 ≥ 2 个 `ReviewDecision`,调用 `aggregate_reviews(...)` 得到 `AggregatedVerdict`,再用 `verdict.rework_targets` 决定 `reopen_task`。
### 3.2 状态与事件
接入需在 `orchestrator/main.py` 增加一个读取开关的 helper(如 `cross_review_enabled()`),与既有 `review_loop_enabled()` 并列;本任务**只新建文件**,故该 helper 的落地留待接入 PR。
### 3.2 评审者来源(≥ 2 独立意见)
当前 `planner.review` 只产出一个意见。接入时的两条候选路径(择一或并用):
1. **多模型 / 多 prompt 评审**:对同一 artifact 用不同 system prompt 或不同 `MASTER_REVIEW_MODEL` 跑 ≥ 2 次,各产出一个 `ReviewDecision`;
2. **同伴评审(peer review)**:复用现有 `build_dispatch_context` 暴露的 `peer_agents` 与 agent 端 `answer_peer_query`(见 `scripts/test-merge-smoke.py::test_agent_peer_shares_summary`),让其它专家就 artifact 给出 `stance/evidence` → 映射为 `ReviewDecision`。
`reviewer_agent_id` 取评审来源标识(模型名或 agent_id),`weight` 可由角色信任度赋值。
### 3.3 状态与事件接线
- **状态**:沿用 `run.metadata`。建议新增 `run.metadata["cross_review"]` 存 `AggregatedVerdict.to_dict()`,`run.metadata["rework_attributions"]` 存 `[ReworkAttribution.to_dict()]`,与既有 `review_cycles` / `review_summary` 并存;持久化走现有 `swarm_runtime.save_run(run)`。
- **事件**:用本模块的 payload builder + 现有 `swarm_runtime.emit_event(run, event_type, payload=...)` 发送。事件类型与现有回调风格一致(`task.*` / `timeline.updated`):
- `review.started`:`review_started_payload(run_id, reviewer_agent_ids, artifact_task_ids, cycle)`;
- `review.decision_made`:`review_decision_made_payload(run_id, verdict, cycle)`;
- `rework.requested`:`rework_requested_payload(run_id, attribution, cycle)`(每个重做目标一条);
- `rework.completed`:`rework_completed_payload(run_id, attribution, cycle, succeeded)`(重做任务到达终态时)。
- **新事件类型须先核对 Manager 回调契约**:HM 的 `agent_callback` 对 `timeline.updated`(需 `title`)、`budget.alert`(需 `threshold_pct`)有必填字段约束(见 `swarm_runtime.emit_event`)。`review.*` / `rework.*` 若要发往 Manager,接入 PR 必须先读 `docs/integration/` 契约确认字段;否则仅作内部事件记录(`_store_event`),不外发回调。
- **替换 vs. augment**:`ENABLE_CROSS_REVIEW=1` 时,`aggregate_reviews` 的 `rework_targets` **替换** `verdict["retry_tasks"]` 作为 `reopen_task` 的输入;其余循环机制(`MAX_REVIEW_CYCLES` 预算、退回 `running`、再 finalize)**复用不变**。关闭时一切照旧。
- **状态**:`run.metadata["cross_review"]` 存 `AggregatedVerdict.to_dict()`,`run.metadata["rework_attributions"]` 存 `[ReworkAttribution.to_dict()]`,与 `review_cycles` 并存(`MAX_REVIEW_CYCLES` 预算复用);拒绝则 `reopen_task` 返工目标、run 退回 `running`。
- **事件不进 Manager 流**:`review.*` / `rework.*` 的 payload builder 已实现但**不经 `emit_event` 外发**(未在 Manager `agent_callback.go` 注册;与 `swarm.health`/`convergence.*` 同策略,避免向订阅全部的回调投递未登记事件)。重开通过既有 `timeline.updated` 反映。登记后方可启用 Manager 侧发送。
### 3.4 不变量
+9 -22
View File
@@ -13,13 +13,9 @@
- **接管(takeover)**:另一个 Agent 请求从当前持有者手中接过任务(持有者卡住、或请求者更合适)。
- **仲裁(arbitrate)**:一个**确定性、可审计**的裁决器在竞价者中选出赢家,并记录理由与落败者。
## 2. 开关(默认关闭)
## 2. 接入现状(已无条件接入,无开关)
| 开关 | 默认 | 行为 |
|---|---|---|
| `ENABLE_TASK_COMPETITION` | **关** | 关闭时派发与现有拉取/分配路径逐字节一致,本模块**不被活体循环触达**(未接线)。开关用于未来接线时的守卫 |
`task_competition_enabled()` 读取该开关。当前模块为**纯逻辑 + 数据模型**,无任何副作用(不碰 Redis / WS / 计费 / 审批链)。
竞争协议是蜂群固有行为,**无 `ENABLE_*` 开关**(本仓即 swarm 运行时)。`main.py` 始终生效的 WS 分支 `task_bid` / `task_yield` / `task_takeover_request` → `handle_task_bid`(累积竞价到 `run.metadata["bids"]`)/ `arbitrate_and_assign`(τ 加权确定性仲裁 → `finalize_dispatch` 择优分派)/ `handle_task_yield`(复用 `release_task`)/ `handle_task_takeover`(须 decisive 胜出方可重分派)。仲裁/竞价/让渡审计存 `run.metadata`(**内部遥测,非 Manager 事件**)。`task_competition.py` 模块本身仍是纯逻辑 + 数据模型(不碰 Redis/WS/计费/审批链)。
## 3. 消息 / 数据模型(`orchestrator/task_competition.py`)
@@ -75,21 +71,12 @@ pip install -r orchestrator/requirements.txt
结果:35/35 PASS(`ALL PASSED`)。
## 集成说明(Integration notes)
## 集成现状(已接入)
以下为**尚未接线**的对接方案(本次不改任何现有文件,仅声明):
已落地于 `orchestrator/main.py`(无开关):
1. **新增 WS 入站消息类型**(`orchestrator/main.py` 的消息分发处,紧邻现有 `peer_message`/`handoff_request` 分支):
- `task_bid` → 解析为 `TaskBid`,缓存到 Redis(建议键 `task_bids:{task_id}` 列表),并 `emit_event(run, *bid_submitted_event(bid))`。
- `task_yield` → 解析为 `TaskYield`,调用 `task_queue.release_task(task_id, agent_id)`(已存在,不增 retry_count),`emit_event(run, *yielded_event(msg))`;若有 `recommend_agent`,作为下轮竞价候选优先项。
- `task_takeover_request` → 解析为 `TaskTakeoverRequest`,`emit_event(run, *takeover_requested_event(req))`;将 `req.bid` 与持有者当前 bid 一并送入 `arbitrate`。
2. **新增编排侧事件类型**(需登记进 Manager 回调契约 `docs/integration/event-schema.md` 与 heicode `agent_callback.go` 的允许事件集,**跨端改动须先读契约**,本次未动):`task.bid_submitted`、`task.yielded`、`task.takeover_requested`、`task.arbitrated`。在登记前,这些事件**不得**直接进入 Manager 事件流(与现有 `peer_message` 仅记内部遥测、不进 HM 流的处理一致)。
3. **新增状态**:竞价窗口(bid window)需要一个收集期 —— 建议在 `task_queue` 旁加「竞价开放/截止」时间戳(新键,不改 `Task` 模型),到期后调用 `arbitrate`,对赢家走现有 `assign_task`,对落败者发拒绝通知。
4. **τ 注入**:`historical_success` 应由 `decision_engine.get_tau(agent_role, agent_id)` 逐竞价者取值后传入 `arbitrate`,使竞争与 ACO 共用同一声誉源。
5. **审批 / 计费 / 审计**:`decisive=False`(接近平局)或接管已分配任务时,须走 Manager 审批链而非自动改派;任何改派保留 `usage` 与 `X-Agent/X-Agnet` 归属。以上均为 heicodeDocs 安全规则要求,接线时必须遵循。
6. **开关守卫**:所有新分支必须包在 `if task_competition_enabled():` 之内,默认关闭以保持 Manager 面接口 / 回调 / 审批链 / 计费语义不变。
1. **WS 入站分支**(紧邻 `peer_message`/`handoff_request`):`task_bid` → `handle_task_bid`(按 agent 去重累积到 `run.metadata["bids"]`);`task_yield` → `handle_task_yield`(复用 `task_queue.release_task`,不增 retry_count);`task_takeover_request` → `handle_task_takeover`(请求者 bid 与持有者中性 bid 一并送 `arbitrate`)。
2. **仲裁分派**:`arbitrate_and_assign(run, task_id)` 取 `run.metadata["bids"]` → `arbitrate` → 赢家经 `finalize_dispatch` 分派;审计存 `run.metadata["arbitrations"]/["yields"]`。
3. **τ 注入**:`historical_success` 由 `decision_engine.get_tau(...)` 逐竞价者取值(经 `normalize_tau`)传入 `arbitrate`,与自选/ACO 共用同一声誉源。
4. **事件不进 Manager 流**:`task.bid_submitted/yielded/takeover_requested/arbitrated` 的 builder 已实现但**不经 `emit_event` 外发**(未在 `agent_callback.go` 注册;与 `peer_message`/`swarm.health` 同策略——仅内部遥测)。登记后方可启用 Manager 侧发送。
5. **审批 / 计费 / 审计(后续硬约束)**:`decisive=False`(接近平局)或接管已分配任务时,按 heicodeDocs 应走 Manager 审批链而非自动改派;当前实现仅在 decisive 胜出时改派,平票不改派(记录待 review)。任何改派保留 `usage` 与 `X-Agent/X-Agnet` 归属。专用「竞价窗口」收集期(定时触发 `arbitrate_and_assign`)为后续优化。
+7 -21
View File
@@ -15,18 +15,16 @@ Design boundaries (read before extending):
- PURE module: no Redis, no WebSocket, no FastAPI imports. The functions take plain data in and
return plain data out, so the orchestrator integrator wires them into the live loop and the
hermetic test exercises them at the module level without any infrastructure.
- DEFAULT-OFF: the whole mechanism is gated by ``ENABLE_AGENT_TASK_PROPOSALS`` (env, default
``false``). ``proposals_enabled()`` is the single read point; when off, an integrator drops
inbound proposal messages and the runtime behaves exactly as before.
- HONESTY (rule #9): this module is real and tested, but it is NOT yet wired into the live WS
loop or the dispatcher. See ``docs/swarm/autonomous-task-generation.md`` §集成说明 for the
exact additions an integrator must apply to shared files (which are intentionally NOT edited
here). What is implemented: the proposal model, the review policy (confidence/budget/dedup),
the accepted->task-spec mapping with full lineage, and the lifecycle-event builders.
- UNCONDITIONAL: agent task proposals are the swarm's ONLY decomposition path — there is no
enable flag (this repo is the swarm runtime; see docs/swarm/decentralized-rework-plan.md). The
WS ``task_proposal`` branch + ``handle_task_proposal`` in main.py are always active.
- HONESTY (rule #9): what this module implements — the proposal model, the review policy
(confidence/budget/dedup), the accepted->task-spec mapping with full lineage, and the
lifecycle-event builders. Wiring into the live loop is in main.py (see
``docs/swarm/autonomous-task-generation.md``).
"""
from __future__ import annotations
import os
import time
import uuid
from dataclasses import dataclass, field, asdict
@@ -34,18 +32,6 @@ from enum import Enum
from typing import Any, Dict, List, Optional, Sequence
# --- feature flag -----------------------------------------------------------------------
def proposals_enabled() -> bool:
"""Whether agent task proposals are accepted (default OFF).
Single read point for ``ENABLE_AGENT_TASK_PROPOSALS``; mirrors the other workflow flags in
main.py (ENABLE_PLANNER_FALLBACK / ENABLE_REVIEW_LOOP / ENABLE_SUBTASK_HANDOFF) so an
integrator gates the inbound WS branch on this and the default behavior is unchanged.
"""
return os.getenv("ENABLE_AGENT_TASK_PROPOSALS", "false").lower() in {"1", "true", "yes"}
# --- constants --------------------------------------------------------------------------
# The Task.source value a proposal-derived task carries. Distinct from "manual"/"planner"/
+7 -23
View File
@@ -22,40 +22,24 @@ This module adds that missing model as a PURE, side-effect-free layer:
* Event-payload builders matching the SwarmRuntime emit_event convention
(Manager `event_type` + plain dict payload).
Default-OFF: nothing here runs unless `ENABLE_CONVERGENCE_REPORT` is set, and
the function is currently NOT wired into `refresh_swarm_run_status`. See
`docs/swarm/convergence-protocol.md` "集成说明(Integration notes)" for exactly
where a caller would compute the report, which events to emit, and how to attach
`termination_reason` to terminal `deployment.status_changed` payloads.
UNCONDITIONAL: `evaluate_convergence` is wired into `refresh_swarm_run_status` and runs on every
terminal swarm run (no enable flag — this repo is the swarm runtime). It produces the report +
`termination_reason` stored on the run and surfaced on `timeline.updated`.
Honesty (org rule #9): the consensus score and the conflict catalogue are a real,
tested mechanism over real run inputs — but they are advisory only until wired in.
This module does not yet change any run's terminal status; the existing
"tasks done + review passed" path remains authoritative.
Honesty (org rule #9): the report is currently EXPLANATORY — it derives `termination_reason` /
consensus / conflicts from real run inputs but does NOT override `run.status` (today next_status
from task bookkeeping and the report agree on completed/failed). Authoritative status-override is a
documented follow-on.
Code is English; the companion design doc is Simplified Chinese (docs/ style).
"""
from __future__ import annotations
import os
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List, Optional
# --- feature flag ---------------------------------------------------------
def convergence_report_enabled() -> bool:
"""Whether the convergence report is computed (default off).
Mirrors the other workflow gates (ENABLE_REVIEW_LOOP, ENABLE_ACO_DISPATCH):
off by default so the Manager contract and existing completion semantics are
unchanged. A caller in refresh_swarm_run_status must check this before
computing or emitting a report.
"""
return os.getenv("ENABLE_CONVERGENCE_REPORT", "false").lower() in {"1", "true", "yes"}
# --- enums ----------------------------------------------------------------
class TerminationReason(str, Enum):
+9 -5
View File
@@ -422,12 +422,16 @@ async def assess_swarm_health(run, *, connected_agent_ids=None):
"connected_agent_caps": caps,
"budget_state": budget_state,
})
run.metadata["health"] = report.to_dict()
await swarm_runtime.save_run(run)
# INTERNAL state only. We deliberately do NOT route this through swarm_runtime.emit_event:
# `swarm.health` is not a registered Manager event, and emit_event can forward to a
# subscribe-all Manager callback (agent_callback.go). Stored on the run (+ a bounded log) so an
# operator/Manager can pull it; registering a Manager-facing health event is a separate contract
# change (event-schema.md).
report_dict = report.to_dict()
run.metadata["health"] = report_dict
if not report.healthy:
await swarm_runtime.emit_event(run, "swarm.health", payload={
"summary": report.summary, "blockers": report.blockers,
})
run.metadata["health_log"] = (run.metadata.get("health_log", [])[-49:] + [report_dict])
await swarm_runtime.save_run(run)
return report
+3 -10
View File
@@ -36,7 +36,6 @@ Code English; companion doc Simplified Chinese, per PROJECT_STANDARD.
from __future__ import annotations
import logging
import os
import time
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
@@ -45,15 +44,9 @@ from pydantic import BaseModel, Field
logger = logging.getLogger(__name__)
# --- feature flag (default OFF) ---------------------------------------------
# The competition protocol is dormant unless explicitly enabled. With the flag off,
# dispatch behaviour is byte-for-byte the existing pull/assign path; nothing in this
# module is reachable from the live loop (it is not wired — see the doc). The flag exists
# so the eventual wiring (a new WS branch) can guard itself.
def task_competition_enabled() -> bool:
"""True only when ENABLE_TASK_COMPETITION is explicitly set (default OFF)."""
return os.getenv("ENABLE_TASK_COMPETITION", "false").lower() in {"1", "true", "yes"}
# UNCONDITIONAL: the competition protocol is the swarm's only contention mechanism (no enable
# flag — this repo is the swarm runtime). The WS task_bid/task_yield/task_takeover_request branches
# + their handlers in main.py are always active.
# --- arbitration policy ------------------------------------------------------
-10
View File
@@ -29,7 +29,6 @@ from orchestrator.autonomous_tasks import (
EVENT_ACCEPTED,
EVENT_REJECTED,
EVENT_MERGED,
proposals_enabled,
review_proposal,
ingest_accepted_proposal,
build_lifecycle_events_for_outcome,
@@ -71,14 +70,6 @@ def make_proposal(**overrides):
return TaskProposal(**kwargs)
def test_flag_default_off():
os.environ.pop("ENABLE_AGENT_TASK_PROPOSALS", None)
check("flag default OFF", proposals_enabled() is False)
os.environ["ENABLE_AGENT_TASK_PROPOSALS"] = "1"
check("flag readable when set", proposals_enabled() is True)
os.environ.pop("ENABLE_AGENT_TASK_PROPOSALS", None)
def test_proposal_invariants():
p = make_proposal()
check("source pinned to agent_proposed", p.source == PROPOSED_SOURCE)
@@ -228,7 +219,6 @@ def test_lifecycle_events():
def main():
test_flag_default_off()
test_proposal_invariants()
test_not_master_origin()
test_accept_path()
-5
View File
@@ -25,7 +25,6 @@ from orchestrator.convergence import ( # noqa: E402
ConflictType,
ConvergenceStatus,
TerminationReason,
convergence_report_enabled,
detect_conflicts,
evaluate_convergence,
event_conflict_detected,
@@ -227,10 +226,6 @@ check("convergence.reached carries termination_reason",
et == "convergence.reached" and payload["termination_reason"] == "tasks_completed")
# --- flag default-off -----------------------------------------------------
check("ENABLE_CONVERGENCE_REPORT default off", convergence_report_enabled() is False)
print()
if failures:
print(f"{len(failures)} convergence check(s) FAILED: {failures}")
+4 -2
View File
@@ -93,7 +93,7 @@ def test_pure():
check("blockers include a detail string", all(b.get("detail") for b in r.blockers))
# ---- orchestrator wrapper records + emits ----
# ---- orchestrator wrapper records the report INTERNALLY (not a Manager event) ----
async def test_wrapper():
await redis_client.connect()
@@ -122,7 +122,9 @@ async def test_wrapper():
refreshed = await swarm_runtime.get_run(run.swarm_id)
check("health report stored on run", bool(refreshed.metadata.get("health"))
and refreshed.metadata["health"]["healthy"] is False)
check("swarm.health event emitted when unhealthy", "swarm.health" in emitted)
check("unhealthy report appended to internal health_log", bool(refreshed.metadata.get("health_log")))
# Contract hygiene: health is INTERNAL — no unregistered Manager event is emitted.
check("no swarm.health Manager event emitted", "swarm.health" not in emitted)
# now register a capable agent → healthy
await agent_registry.register_agent("rust-agent", ["rust"])
-6
View File
@@ -26,7 +26,6 @@ from orchestrator.task_competition import (
arbitrated_event,
bid_submitted_event,
takeover_requested_event,
task_competition_enabled,
yielded_event,
)
@@ -40,11 +39,6 @@ def check(name, cond):
def main():
# --- flag is default OFF -------------------------------------------------
check("flag default OFF", task_competition_enabled() is False)
os.environ["ENABLE_TASK_COMPETITION"] = "1"
check("flag readable when set", task_competition_enabled() is True)
task_id = "task-42"
required = ["python", "testing"]
# τ map: the strong agent has earned reputation, the weak one has not (→ neutral 0.5).