[moz] impl(§22): P1+P2 轻量路径+数据流+Round Review Gitea 适配
CI / lint (pull_request) Successful in 47s
CI / test (pull_request) Successful in 1m0s
CI / frontend (pull_request) Failing after 46s
CI / notify-on-failure (pull_request) Successful in 2s

§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:
cfdaily
2026-06-23 23:46:20 +08:00
parent 493d0f017b
commit 35959e19fa
4 changed files with 140 additions and 28 deletions
+25 -23
View File
@@ -1,9 +1,10 @@
--- ---
title: "End-to-End Flow — 端到端任务流程设计" title: "End-to-End Flow — 端到端任务流程设计"
created: 2026-06-22 created: 2026-06-22
version: v1.1 version: v1.2
status: draft 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 初版 v1.0 初版
--- ---
@@ -76,7 +77,7 @@ Gitea: Issue created (no assignee, label type/feat)
| 维度 | 内容 | | 维度 | 内容 |
|------|------| |------|------|
| **触发** | ticker 30s 扫到 Phase 0 创建的 pending task | | **触发** | ticker 30s 扫到 Phase 0 创建的 pending task |
| **daemon 函数** | `ticker._dispatch_pending``dispatcher.decide``_broadcast_claim`⚠️ 当前 broadcast 使用 claim prompt(黑板 API)而非 discussion promptP0 修复后改为 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 | | **daemon 行为** | assignee=None → router 返回 mode=delegate → ticker 归入 broadcast_tasks → 广播给所有空闲 agent |
| **prompt 来源(设计期望)** | `discussion prompt`(§13.2):你是谁 + 你必须做什么(4 维度)+ Gitea API + Boids 行为准则 | | **prompt 来源(设计期望)** | `discussion prompt`(§13.2):你是谁 + 你必须做什么(4 维度)+ Gitea API + Boids 行为准则 |
| **agent 行为(设计期望)** | 每个 agent 在 **Gitea Issue** comment(角色名开头,4 维度回应);需要参与的 agent 创建 sub IssueGitea APIassign 自己);在 parent Issue comment 注册 sub | | **agent 行为(设计期望)** | 每个 agent 在 **Gitea Issue** comment(角色名开头,4 维度回应);需要参与的 agent 创建 sub IssueGitea APIassign 自己);在 parent Issue comment 注册 sub |
@@ -112,9 +113,9 @@ Gitea: Issue created (no assignee, label type/feat)
- 创建 sub Issue: POST /repos/{repo}/issues - 创建 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
- 如果没有任何 agent 创建 sub Issue → ticker 升级庞统(3 轮无 taker 机制) - 如果没有任何 agent 创建 sub Issue → ticker 升级庞统(3 轮无 taker 机制)
@@ -222,7 +223,7 @@ issue_assigned:
| Phase | prompt 用途 | 设计指定的模板来源 | 当前实际来源 | 一致? | | Phase | prompt 用途 | 设计指定的模板来源 | 当前实际来源 | 一致? |
|-------|-----------|----------------|------------|-------| |-------|-----------|----------------|------------|-------|
| 1 Discussion | 广播讨论 | discussion prompt(§13.2Gitea API | claim prompt`_build_claim_prompt`黑板 API | | | 1 Discussion | 广播讨论 | discussion prompt(§13.2Gitea API | discussion prompt`_build_discussion_prompt`Gitea API | |
| 2 Executor | 编码执行 | `ToolchainHandler.build_prompt` + YAML steps | 同设计 | ✅ | | 2 Executor | 编码执行 | `ToolchainHandler.build_prompt` + YAML steps | 同设计 | ✅ |
| 3 CI 失败 | 修复 CI | `ToolchainHandler.build_prompt` ci_failure | 同设计 | ✅ | | 3 CI 失败 | 修复 CI | `ToolchainHandler.build_prompt` ci_failure | 同设计 | ✅ |
| 4 Review | 审查 PR | `ToolchainHandler.build_prompt` review_request | 同设计 | ✅ | | 4 Review | 审查 PR | `ToolchainHandler.build_prompt` review_request | 同设计 | ✅ |
@@ -230,7 +231,7 @@ issue_assigned:
| 6 Round Review | 庞统三问 | `ticker._build_review_prompt` | 同设计 | ✅ | | 6 Round Review | 庞统三问 | `ticker._build_review_prompt` | 同设计 | ✅ |
| 7 Issue closed | 关闭通知 | `ToolchainHandler.build_prompt` issue_closed | 同设计 | ✅ | | 7 Issue closed | 关闭通知 | `ToolchainHandler.build_prompt` issue_closed | 同设计 | ✅ |
**唯一偏差在 Phase 1**discussion 设计了完整的 prompt(§13.2),但 ticker 从未调用它 **唯一偏差已修复**PR #124 将 ticker broadcast 改为根据 action_type 选择 discussion promptGitea API)或 claim prompt(黑板 API
--- ---
@@ -239,12 +240,12 @@ issue_assigned:
| Phase | 实现状态 | 差距描述 | | Phase | 实现状态 | 差距描述 |
|-------|---------|---------| |-------|---------|---------|
| 0 parent Issue 创建 | ✅ 已实现 | PR #113 `_handle_issues` opened 分支,无 assignee + type/* label → toolchain task | | 0 parent Issue 创建 | ✅ 已实现 | PR #113 `_handle_issues` opened 分支,无 assignee + type/* label → toolchain task |
| 1 Discussion 广播 | **实现** | ticker broadcast 只有 `_build_claim_prompt`(黑板 API,认领模式),没有 discussion broadcastGitea API,讨论模式)。`_build_discussion_prompt` 存在于 spawner 中但 ticker 从未调用。agent 收到的是"认领并执行"而不是"讨论后创建 sub" | | 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,走不到此阶段 | | 2 sub Issue → executor | ✅ 已实现 | assigned 路径 + YAML steps 已实现(PR #107),Phase 1 修复后 agent 会创建 sub Issue → 走到此阶段 |
| 3 PR + CI | ✅ 已实现 | toolchain handler 正常处理 PR opened + CI 失败 | | 3 PR + CI | ✅ 已实现 | toolchain handler 正常处理 PR opened + CI 失败 |
| 4 Review | ✅ 已实现 | Review 请求 + Review 结果通知正常 | | 4 Review | ✅ 已实现 | Review 请求 + Review 结果通知正常 |
| 5 Merge + sub 关闭 | ⚠️ 部分 | merge 通知正常。但 agent 创建的 PR body 不含 `Closes #N`(因为走的是 claim prompt 不是 executor promptprompt 中没有 Closes #N 约束) | | 5 Merge + sub 关闭 | ✅ 已实现 | merge 通知正常。executor promptYAML steps)中已包含 `Closes #{issue_number}` |
| 6 Round Review | ❌ 未适配 | `_check_round_complete` 扫黑板 `parent_task` 字段,不扫 Gitea parent/sub Issue 映射。没有 sub Issue 就检测不到 | | 6 Round Review | **已实现** | `_check_round_complete` 支持双源扫描:黑板 `tasks.parent_task` + toolchain `task_state.parent_issue` |
| 7 parent Issue 关闭 | ✅ 已实现 | PR #113 issue_closed auto-pass | | 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 promptGitea API 版本)而非 claim prompt | | **P0** | Phase 1 discussion broadcast | **已完成**PR #124 | ~~核心断裂~~ 已修复:ticker 判断 action_type=issue_discussion → discussion prompt |
| **P1** | Phase 5 PR body Closes #N | sub Issue 不自动关闭 | Phase 2 executor prompt 中已有 Closes #N 约束(YAML steps),Phase 1后自然解决 | | **P1** | Phase 5 PR body Closes #N | **已完成** | YAML steps 已包含 Closes #NP0后自然走通 |
| **P2** | Phase 6 Round Review Gitea 适配 | 无法触发庞统三问 | `_check_round_complete` 改为扫 Gitea parent/sub Issue 映射,详见 §22.7 数据流设计 | | **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 路径 ✅ - `opened`(无 assignee + type/* label)→ discussion task ← Discussion 路径 ✅
- `assigned`(有 assignee)→ executor task ← Direct 路径 ✅ - `assigned`(有 assignee)→ executor task ← Direct 路径 ✅
**轻量路径的后端已实现**需要补充的是 **轻量路径已全部实现**包括
1. `flow/direct``flow/discuss` label 的识别逻辑(`toolchain_routes.py` `_handle_issues` opened 分支 1. `flow/direct``flow/discuss` label 已创建(Gitea label id=100, 101
2. Discussion prompt 中告知 agent 可以建议 direct assignment(降级机制) 2. `toolchain_routes.py` `_handle_issues` opened 分支识别 `flow/direct` → executor task
3. ✅ Discussion prompt 中包含降级机制引导(§22.6)
--- ---
@@ -331,12 +333,12 @@ Discussion 进行中,如果所有 agent 都认为任务足够简单只涉及
### 当前状态 vs 设计目标 ### 当前状态 vs 设计目标
| 数据 | 当前存储 | §20/§21 设计目标 | 差距 | | 数据 | 当前存储 | §20/§21 设计目标 | 状态 |
|------|---------|-----------------|------| |------|---------|-----------------|------|
| 协作面(title/body/comment | 黑板 DB tasks/comments 表 | Gitea Issue/PR | ❌ Phase 1 断裂导致 agent 用黑板 API | | 协作面(title/body/comment | Gitea Issue/PRPhase 1 已修复) | Gitea Issue/PR | ✅ |
| parent/sub 映射 | `tasks.parent_task`黑板字段 | `task_state.parent_issue`(新表) | ❌ task_state 表不存在 | | parent/sub 映射 | `task_state.parent_issue`(已实现) + `tasks.parent_task`兼容 | `task_state.parent_issue`(新表) | |
| 执行状态 | 黑板 DB tasks.status | task_state.status | task_state 表不存在 | | 执行状态 | toolchain DB tasks.status + task_state.status | task_state.status | ✅ |
| 成果物 | 黑板 DB outputs 表 | git commit + PR | ⚠️ 需 prompt 引导 | | 成果物 | git commit + PR | git commit + PR | |
### parent/sub Issue 映射数据流(设计) ### parent/sub Issue 映射数据流(设计)
+80 -3
View File
@@ -23,7 +23,7 @@ from typing import Any, Dict, List, Optional, Set, Tuple
import httpx import httpx
from fastapi import APIRouter, Header, Request, Response 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.models import Task
from src.blackboard.operations import Blackboard from src.blackboard.operations import Blackboard
from src.config.agents import AGENT_IDS from src.config.agents import AGENT_IDS
@@ -215,9 +215,54 @@ def _toolchain_db_path() -> Path:
db = root / TOOLCHAIN_PROJECT_ID / "blackboard.db" db = root / TOOLCHAIN_PROJECT_ID / "blackboard.db"
db.parent.mkdir(parents=True, exist_ok=True) db.parent.mkdir(parents=True, exist_ok=True)
init_db(db) init_db(db)
_init_task_state_table(db)
return 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( def _send_toolchain_task(
to_agent: str | None, to_agent: str | None,
title: str, title: str,
@@ -1085,6 +1130,14 @@ async def _handle_issues(payload: Dict[str, Any]) -> None:
}, },
) )
# §22.7: 解析 [parent #{N}] 写入 task_statetoolchain 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": elif action == "closed":
# §21 §11 Issue closed 纯通知(auto-pass # §21 §11 Issue closed 纯通知(auto-pass
assignee_login = "" assignee_login = ""
@@ -1113,14 +1166,38 @@ async def _handle_issues(payload: Dict[str, Any]) -> None:
assignees = list(assignees) + [single_assignee] assignees = list(assignees) + [single_assignee]
if not assignees and not ("部署失败" in issue_title): if not assignees and not ("部署失败" in issue_title):
# 无 assignee + 非部署失败 → 检查是否有 type/* label # 无 assignee + 非部署失败 → 检查 flow/* 和 type/* label
labels_list_opened = [ labels_list_opened = [
lbl.get("name", "") for lbl in (issue.get("labels") or []) lbl.get("name", "") for lbl in (issue.get("labels") or [])
] ]
has_type_label = any( has_type_label = any(
lbl.lower().startswith("type/") for lbl in labels_list_opened 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 taskdaemon 不自动 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 "(无描述)" issue_body = issue.get("body", "") or "(无描述)"
title_discussion = f"Issue 讨论: {issue_title} ({repo}#{issue_number})" title_discussion = f"Issue 讨论: {issue_title} ({repo}#{issue_number})"
_send_toolchain_task( _send_toolchain_task(
+3
View File
@@ -165,6 +165,9 @@ DISCUSSION_PROMPT_TEMPLATE = """你被 spawn 来参与 Gitea Issue 讨论。
- 如果讨论收敛到可执行的任务,直接创建 sub Issue(assign 自己) - 如果讨论收敛到可执行的任务,直接创建 sub Issue(assign 自己)
- 如果有分歧或不确定,在 Issue comment @pangtong-fujunshi 裁决 - 如果有分歧或不确定,在 Issue comment @pangtong-fujunshi 裁决
- **降级机制**:如果你认为这个任务足够简单、只涉及一个角色,可以在 comment 中建议:
`@pangtong-fujunshi 建议直接指派 @agent-id,理由:...`
庞统判断后会创建 sub Issue 直接 assign 给该 agent
- 标记完成(在 parent Issue comment 写总结) - 标记完成(在 parent Issue comment 写总结)
""" """
+32 -2
View File
@@ -28,6 +28,15 @@ from src.blackboard.queries import Queries
from src.blackboard.registry import ProjectRegistry 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 @dataclass
class BroadcastRound: class BroadcastRound:
"""追踪单个任务的广播状态""" """追踪单个任务的广播状态"""
@@ -417,8 +426,29 @@ class Ticker:
finally: finally:
conn.close() conn.close()
for row in parent_rows: # §22.7: 同时扫描 task_state 中的 parent_issueGitea 路径)
parent_id = row["parent_task"] 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: try:
summary = bb.get_subtasks_summary(parent_id) summary = bb.get_subtasks_summary(parent_id)
if not summary or not summary["all_terminal"]: if not summary or not summary["all_terminal"]: