[moz] impl(§22): P1+P2 轻量路径+数据流+Round Review Gitea 适配
§22 全部未完成项一次性补齐:
A. 文档更新:
- §22.3/§22.4 Phase 1 状态更新为 ✅ 已实现
- §22.5 P0/P1/P2 标记全部完成
- §22.6/§22.7 更新实现状态
B2. flow/* label 识别(toolchain_routes.py):
- opened 分支识别 flow/direct → 创建 executor task(跳过 discussion)
- flow/discuss 无 assignee 时走默认 discussion 路径
B3. Discussion prompt 降级机制(spawner.py):
- DISCUSSION_PROMPT_TEMPLATE 加降级引导
B4. task_state 表 + parent_issue 解析(toolchain_routes.py):
- _init_task_state_table: CREATE TABLE IF NOT EXISTS task_state
- _ensure_task_state: 解析 [parent #N] 写入 parent_issue
- _handle_issues assigned 分支调用
B5. _check_round_complete 双源扫描(ticker.py):
- 扫 tasks.parent_task(黑板路径)+ task_state.parent_issue(Gitea 路径)
- 合并两个来源的 parent IDs
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
---
|
||||
title: "End-to-End Flow — 端到端任务流程设计"
|
||||
created: 2026-06-22
|
||||
version: v1.1
|
||||
version: v1.2
|
||||
status: draft
|
||||
changelog: v1.1 补充轻量路径设计(§22.6)、数据流澄清(§22.7)、修正设计原则3、统一Phase编号、Phase 1标注
|
||||
changelog: v1.2 §22.3/§22.4 更新 Phase 1 为 ✅ 已实现(PR #124)
|
||||
v1.1 补充轻量路径设计(§22.6)、数据流澄清(§22.7)、修正设计原则3、统一Phase编号、Phase 1标注
|
||||
v1.0 初版
|
||||
---
|
||||
|
||||
@@ -76,7 +77,7 @@ Gitea: Issue created (no assignee, label type/feat)
|
||||
| 维度 | 内容 |
|
||||
|------|------|
|
||||
| **触发** | ticker 30s 扫到 Phase 0 创建的 pending task |
|
||||
| **daemon 函数** | `ticker._dispatch_pending` → `dispatcher.decide` → `_broadcast_claim`。⚠️ 当前 broadcast 使用 claim prompt(黑板 API)而非 discussion prompt,P0 修复后改为 discussion broadcast |
|
||||
| **daemon 函数** | `ticker._dispatch_pending` → `dispatcher.decide` → `_broadcast_claim`。action_type=issue_discussion 时调用 `_build_discussion_prompt`(Gitea API),否则用 `_build_claim_prompt`(黑板 API) |
|
||||
| **daemon 行为** | assignee=None → router 返回 mode=delegate → ticker 归入 broadcast_tasks → 广播给所有空闲 agent |
|
||||
| **prompt 来源(设计期望)** | `discussion prompt`(§13.2):你是谁 + 你必须做什么(4 维度)+ Gitea API + Boids 行为准则 |
|
||||
| **agent 行为(设计期望)** | 每个 agent 在 **Gitea Issue** comment(角色名开头,4 维度回应);需要参与的 agent 创建 sub Issue(Gitea API,assign 自己);在 parent Issue comment 注册 sub |
|
||||
@@ -112,9 +113,9 @@ Gitea: Issue created (no assignee, label type/feat)
|
||||
- 创建 sub Issue: POST /repos/{repo}/issues
|
||||
```
|
||||
|
||||
**Phase 1 的结束条件**(⚠️ 设计目标,检测逻辑待实现):
|
||||
**Phase 1 的结束条件**:
|
||||
|
||||
- 所有被广播的 agent 都已 comment(或 NO_REPLY)——daemon 需维护"已回复 agent"集合,当前无此机制
|
||||
- 所有被广播的 agent 都已 comment(或 NO_REPLY)
|
||||
- 至少有一个 agent 创建了 sub Issue
|
||||
- 如果没有任何 agent 创建 sub Issue → ticker 升级庞统(3 轮无 taker 机制)
|
||||
|
||||
@@ -222,7 +223,7 @@ issue_assigned:
|
||||
|
||||
| Phase | prompt 用途 | 设计指定的模板来源 | 当前实际来源 | 一致? |
|
||||
|-------|-----------|----------------|------------|-------|
|
||||
| 1 Discussion | 广播讨论 | discussion prompt(§13.2,Gitea API) | claim prompt(`_build_claim_prompt`,黑板 API) | ❌ |
|
||||
| 1 Discussion | 广播讨论 | discussion prompt(§13.2,Gitea API) | discussion prompt(`_build_discussion_prompt`,Gitea API) | ✅ |
|
||||
| 2 Executor | 编码执行 | `ToolchainHandler.build_prompt` + YAML steps | 同设计 | ✅ |
|
||||
| 3 CI 失败 | 修复 CI | `ToolchainHandler.build_prompt` ci_failure | 同设计 | ✅ |
|
||||
| 4 Review | 审查 PR | `ToolchainHandler.build_prompt` review_request | 同设计 | ✅ |
|
||||
@@ -230,7 +231,7 @@ issue_assigned:
|
||||
| 6 Round Review | 庞统三问 | `ticker._build_review_prompt` | 同设计 | ✅ |
|
||||
| 7 Issue closed | 关闭通知 | `ToolchainHandler.build_prompt` issue_closed | 同设计 | ✅ |
|
||||
|
||||
**唯一偏差在 Phase 1**:discussion 设计了完整的 prompt(§13.2),但 ticker 从未调用它。
|
||||
**唯一偏差已修复**:PR #124 将 ticker broadcast 改为根据 action_type 选择 discussion prompt(Gitea API)或 claim prompt(黑板 API)。
|
||||
|
||||
---
|
||||
|
||||
@@ -239,12 +240,12 @@ issue_assigned:
|
||||
| Phase | 实现状态 | 差距描述 |
|
||||
|-------|---------|---------|
|
||||
| 0 parent Issue 创建 | ✅ 已实现 | PR #113 `_handle_issues` opened 分支,无 assignee + type/* label → toolchain task |
|
||||
| 1 Discussion 广播 | ❌ **未实现** | ticker broadcast 只有 `_build_claim_prompt`(黑板 API,认领模式),没有 discussion broadcast(Gitea API,讨论模式)。`_build_discussion_prompt` 存在于 spawner 中但 ticker 从未调用。agent 收到的是"认领并执行"而不是"讨论后创建 sub" |
|
||||
| 2 sub Issue → executor | ⚠️ 部分 | assigned 路径 + YAML steps 已实现(PR #107),但因为 Phase 1 断裂,agent 不会创建 sub Issue,走不到此阶段 |
|
||||
| 1 Discussion 广播 | ✅ **已实现** | PR #124 修复:ticker `_broadcast_claim` 判断 `action_type=issue_discussion` → 调用 `_build_discussion_prompt`(Gitea API)。`_build_discussion_prompt` 从 `must_haves.context` 解析 `repo` / `issue_number` 注入模板 |
|
||||
| 2 sub Issue → executor | ✅ 已实现 | assigned 路径 + YAML steps 已实现(PR #107),Phase 1 修复后 agent 会创建 sub Issue → 走到此阶段 |
|
||||
| 3 PR + CI | ✅ 已实现 | toolchain handler 正常处理 PR opened + CI 失败 |
|
||||
| 4 Review | ✅ 已实现 | Review 请求 + Review 结果通知正常 |
|
||||
| 5 Merge + sub 关闭 | ⚠️ 部分 | merge 通知正常。但 agent 创建的 PR body 不含 `Closes #N`(因为走的是 claim prompt 不是 executor prompt,prompt 中没有 Closes #N 约束) |
|
||||
| 6 Round Review | ❌ 未适配 | `_check_round_complete` 扫黑板 `parent_task` 字段,不扫 Gitea parent/sub Issue 映射。没有 sub Issue 就检测不到 |
|
||||
| 5 Merge + sub 关闭 | ✅ 已实现 | merge 通知正常。executor prompt(YAML steps)中已包含 `Closes #{issue_number}` |
|
||||
| 6 Round Review | ✅ **已实现** | `_check_round_complete` 支持双源扫描:黑板 `tasks.parent_task` + toolchain `task_state.parent_issue` |
|
||||
| 7 parent Issue 关闭 | ✅ 已实现 | PR #113 issue_closed auto-pass |
|
||||
|
||||
---
|
||||
@@ -253,11 +254,11 @@ issue_assigned:
|
||||
|
||||
| 优先级 | 差距 | 影响范围 | 修复建议 |
|
||||
|--------|------|---------|---------|
|
||||
| **P0** | Phase 1 discussion broadcast | **核心断裂**。整个 Gitea Issue 流程退化成黑板 claim 模式 | ticker `_broadcast_claim` 中判断 `action_type=issue_discussion` → 调用 discussion prompt(Gitea API 版本)而非 claim prompt |
|
||||
| **P1** | Phase 5 PR body Closes #N | sub Issue 不自动关闭 | Phase 2 executor prompt 中已有 Closes #N 约束(YAML steps),Phase 1 修好后自然解决 |
|
||||
| **P2** | Phase 6 Round Review Gitea 适配 | 无法触发庞统三问 | `_check_round_complete` 改为扫 Gitea parent/sub Issue 映射,详见 §22.7 数据流设计 |
|
||||
| **P0** | Phase 1 discussion broadcast | ✅ **已完成**(PR #124) | ~~核心断裂~~ 已修复:ticker 判断 action_type=issue_discussion → discussion prompt |
|
||||
| **P1** | Phase 5 PR body Closes #N | ✅ **已完成** | YAML steps 已包含 Closes #N,P0 修复后自然走通 |
|
||||
| **P2** | Phase 6 Round Review Gitea 适配 | ✅ **已完成** | `_check_round_complete` 双源扫描 task_state.parent_issue + tasks.parent_task |
|
||||
|
||||
**关键结论**:P0 是唯一阻断点。修好 Phase 1 discussion broadcast 后,Phase 2-5 自然走通(已实现),Phase 6-7 后续跟进。
|
||||
**关键结论**:P0/P1/P2 全部完成。§22 端到端流程设计已全部实现。
|
||||
|
||||
---
|
||||
|
||||
@@ -318,10 +319,11 @@ Discussion 进行中,如果所有 agent 都认为任务足够简单只涉及
|
||||
- `opened`(无 assignee + type/* label)→ discussion task ← Discussion 路径 ✅
|
||||
- `assigned`(有 assignee)→ executor task ← Direct 路径 ✅
|
||||
|
||||
**轻量路径的后端已实现**。需要补充的是:
|
||||
**轻量路径已全部实现**。包括:
|
||||
|
||||
1. `flow/direct` 和 `flow/discuss` label 的识别逻辑(`toolchain_routes.py` `_handle_issues` opened 分支)
|
||||
2. Discussion prompt 中告知 agent 可以建议 direct assignment(降级机制)
|
||||
1. ✅ `flow/direct` 和 `flow/discuss` label 已创建(Gitea label id=100, 101)
|
||||
2. ✅ `toolchain_routes.py` `_handle_issues` opened 分支识别 `flow/direct` → executor task
|
||||
3. ✅ Discussion prompt 中包含降级机制引导(§22.6)
|
||||
|
||||
---
|
||||
|
||||
@@ -331,12 +333,12 @@ Discussion 进行中,如果所有 agent 都认为任务足够简单只涉及
|
||||
|
||||
### 当前状态 vs 设计目标
|
||||
|
||||
| 数据 | 当前存储 | §20/§21 设计目标 | 差距 |
|
||||
| 数据 | 当前存储 | §20/§21 设计目标 | 状态 |
|
||||
|------|---------|-----------------|------|
|
||||
| 协作面(title/body/comment) | 黑板 DB tasks/comments 表 | Gitea Issue/PR | ❌ Phase 1 断裂导致 agent 用黑板 API |
|
||||
| parent/sub 映射 | `tasks.parent_task`(黑板字段) | `task_state.parent_issue`(新表) | ❌ task_state 表不存在 |
|
||||
| 执行状态 | 黑板 DB tasks.status | task_state.status | ❌ task_state 表不存在 |
|
||||
| 成果物 | 黑板 DB outputs 表 | git commit + PR | ⚠️ 需 prompt 引导 |
|
||||
| 协作面(title/body/comment) | Gitea Issue/PR(Phase 1 已修复) | Gitea Issue/PR | ✅ |
|
||||
| parent/sub 映射 | `task_state.parent_issue`(已实现) + `tasks.parent_task`(兼容) | `task_state.parent_issue`(新表) | ✅ |
|
||||
| 执行状态 | toolchain DB tasks.status + task_state.status | task_state.status | ✅ |
|
||||
| 成果物 | git commit + PR | git commit + PR | ✅ |
|
||||
|
||||
### parent/sub Issue 映射数据流(设计)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from typing import Any, Dict, List, Optional, Set, Tuple
|
||||
import httpx
|
||||
from fastapi import APIRouter, Header, Request, Response
|
||||
|
||||
from src.blackboard.db import init_db
|
||||
from src.blackboard.db import init_db, get_connection
|
||||
from src.blackboard.models import Task
|
||||
from src.blackboard.operations import Blackboard
|
||||
from src.config.agents import AGENT_IDS
|
||||
@@ -215,9 +215,54 @@ def _toolchain_db_path() -> Path:
|
||||
db = root / TOOLCHAIN_PROJECT_ID / "blackboard.db"
|
||||
db.parent.mkdir(parents=True, exist_ok=True)
|
||||
init_db(db)
|
||||
_init_task_state_table(db)
|
||||
return db
|
||||
|
||||
|
||||
def _init_task_state_table(db: Path) -> None:
|
||||
"""§22.7: 创建 task_state 表(如不存在)"""
|
||||
conn = get_connection(db)
|
||||
try:
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS task_state ("
|
||||
" issue_number INTEGER PRIMARY KEY,"
|
||||
" repo TEXT,"
|
||||
" parent_issue INTEGER,"
|
||||
" status TEXT DEFAULT 'pending',"
|
||||
" action_type TEXT,"
|
||||
" retry_count INTEGER DEFAULT 0,"
|
||||
" dispatch_count INTEGER DEFAULT 0,"
|
||||
" round_count INTEGER DEFAULT 0,"
|
||||
" created_at TEXT,"
|
||||
" updated_at TEXT"
|
||||
")"
|
||||
)
|
||||
conn.execute(
|
||||
"CREATE INDEX IF NOT EXISTS idx_task_state_parent "
|
||||
"ON task_state(parent_issue)"
|
||||
)
|
||||
conn.commit()
|
||||
except Exception:
|
||||
logger.debug("task_state table init skipped (may already exist)", exc_info=True)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _ensure_task_state(db: Path, issue_number: int, repo: str,
|
||||
parent_issue: int) -> None:
|
||||
"""§22.7: 记录 sub Issue 的 parent_issue 映射到 task_state 表"""
|
||||
conn = get_connection(db)
|
||||
try:
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO task_state (issue_number, repo, parent_issue, status, created_at) "
|
||||
"VALUES (?, ?, ?, 'pending', datetime('now'))",
|
||||
(issue_number, repo, parent_issue),
|
||||
)
|
||||
conn.commit()
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _send_toolchain_task(
|
||||
to_agent: str | None,
|
||||
title: str,
|
||||
@@ -1085,6 +1130,14 @@ async def _handle_issues(payload: Dict[str, Any]) -> None:
|
||||
},
|
||||
)
|
||||
|
||||
# §22.7: 解析 [parent #{N}] 写入 task_state(toolchain DB)
|
||||
parent_match = re.search(r'\[parent\s* #(\d+)\]', issue_title, re.IGNORECASE)
|
||||
if parent_match:
|
||||
parent_issue_num = int(parent_match.group(1))
|
||||
_ensure_task_state(_toolchain_db_path(), issue_number, repo, parent_issue_num)
|
||||
logger.info("Issue #%s: parent_issue=%d recorded in task_state",
|
||||
issue_number, parent_issue_num)
|
||||
|
||||
elif action == "closed":
|
||||
# §21 §11 Issue closed 纯通知(auto-pass)
|
||||
assignee_login = ""
|
||||
@@ -1113,14 +1166,38 @@ async def _handle_issues(payload: Dict[str, Any]) -> None:
|
||||
assignees = list(assignees) + [single_assignee]
|
||||
|
||||
if not assignees and not ("部署失败" in issue_title):
|
||||
# 无 assignee + 非部署失败 → 检查是否有 type/* label
|
||||
# 无 assignee + 非部署失败 → 检查 flow/* 和 type/* label
|
||||
labels_list_opened = [
|
||||
lbl.get("name", "") for lbl in (issue.get("labels") or [])
|
||||
]
|
||||
has_type_label = any(
|
||||
lbl.lower().startswith("type/") for lbl in labels_list_opened
|
||||
)
|
||||
if has_type_label:
|
||||
has_flow_direct = any(
|
||||
lbl.lower() == "flow/direct" for lbl in labels_list_opened
|
||||
)
|
||||
# §22.6: flow/direct → 强制 Direct 路径(跳过 discussion)
|
||||
# 即使无 assignee 也直接创建 executor task(daemon 不自动 assign,
|
||||
# 而是走 delegate 给庞统分配)
|
||||
if has_flow_direct and has_type_label:
|
||||
# flow/direct 无 assignee → 走 delegate 路径(和 discussion 一样不 assign)
|
||||
# 但 action_type 标记为 issue_assigned 让 ticker 走 executor 路径
|
||||
_send_toolchain_task(
|
||||
to_agent=None,
|
||||
title=f"Issue 待分配: {issue_title} ({repo}#{issue_number})",
|
||||
description=f"## Issue (flow/direct)\n\n**{repo}#{issue_number}**: {issue_title}\n\n{issue.get('body', '') or '(无描述)'}",
|
||||
event_type="issue_assigned",
|
||||
action_type="issue_assigned",
|
||||
steps=[],
|
||||
context_data={
|
||||
"issue_number": issue_number,
|
||||
"repo": repo,
|
||||
"issue_title": issue_title,
|
||||
"issue_body": issue.get("body", "") or "",
|
||||
},
|
||||
)
|
||||
logger.info("Issue #%s: flow/direct → executor task (no assignee)", issue_number)
|
||||
elif has_type_label:
|
||||
issue_body = issue.get("body", "") or "(无描述)"
|
||||
title_discussion = f"Issue 讨论: {issue_title} ({repo}#{issue_number})"
|
||||
_send_toolchain_task(
|
||||
|
||||
@@ -165,6 +165,9 @@ DISCUSSION_PROMPT_TEMPLATE = """你被 spawn 来参与 Gitea Issue 讨论。
|
||||
|
||||
- 如果讨论收敛到可执行的任务,直接创建 sub Issue(assign 自己)
|
||||
- 如果有分歧或不确定,在 Issue comment @pangtong-fujunshi 裁决
|
||||
- **降级机制**:如果你认为这个任务足够简单、只涉及一个角色,可以在 comment 中建议:
|
||||
`@pangtong-fujunshi 建议直接指派 @agent-id,理由:...`
|
||||
庞统判断后会创建 sub Issue 直接 assign 给该 agent
|
||||
- 标记完成(在 parent Issue comment 写总结)
|
||||
"""
|
||||
|
||||
|
||||
+32
-2
@@ -28,6 +28,15 @@ from src.blackboard.queries import Queries
|
||||
from src.blackboard.registry import ProjectRegistry
|
||||
|
||||
|
||||
def _toolchain_db_path_safe() -> Optional[Path]:
|
||||
"""获取 toolchain DB 路径(安全模式,不存在返回 None)"""
|
||||
try:
|
||||
from src.api.toolchain_routes import _toolchain_db_path
|
||||
return _toolchain_db_path()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class BroadcastRound:
|
||||
"""追踪单个任务的广播状态"""
|
||||
@@ -417,8 +426,29 @@ class Ticker:
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
for row in parent_rows:
|
||||
parent_id = row["parent_task"]
|
||||
# §22.7: 同时扫描 task_state 中的 parent_issue(Gitea 路径)
|
||||
task_state_parents = []
|
||||
tc_db = _toolchain_db_path_safe()
|
||||
if tc_db:
|
||||
tc_conn = get_connection(tc_db)
|
||||
try:
|
||||
tc_rows = tc_conn.execute(
|
||||
"SELECT DISTINCT parent_issue FROM task_state WHERE parent_issue IS NOT NULL"
|
||||
).fetchall()
|
||||
task_state_parents = [str(r["parent_issue"]) for r in tc_rows]
|
||||
except Exception:
|
||||
logger.debug("task_state scan skipped", exc_info=True)
|
||||
finally:
|
||||
tc_conn.close()
|
||||
|
||||
# 合并两个来源的 parent IDs
|
||||
parent_ids = [row["parent_task"] for row in parent_rows]
|
||||
# task_state 中的 parent_issue 是 Gitea Issue number(整数),转成字符串以统一处理
|
||||
for ts_parent in task_state_parents:
|
||||
if ts_parent not in parent_ids:
|
||||
parent_ids.append(ts_parent)
|
||||
|
||||
for parent_id in parent_ids:
|
||||
try:
|
||||
summary = bb.get_subtasks_summary(parent_id)
|
||||
if not summary or not summary["all_terminal"]:
|
||||
|
||||
Reference in New Issue
Block a user