diff --git a/src/daemon/ticker.py b/src/daemon/ticker.py index 4ebf211..f72681a 100644 --- a/src/daemon/ticker.py +++ b/src/daemon/ticker.py @@ -363,9 +363,176 @@ class Ticker: return refreshed # ------------------------------------------------------------------ - # 依赖推进 + # 一轮结束检测 + 庞统 review (v2.9 #01) # ------------------------------------------------------------------ + MAX_ROUNDS = 5 # §4.5 防无限循环 + + async def _check_round_complete(self, db_path: Path, + project_id: str) -> List[str]: + """检测 parent task 下所有 sub task 终态 → spawn 庞统 review + + 流程(§4.4): + 1. 扫描所有 parent task + 2. 对每个 parent 检查 sub task 是否全部终态 + 3. 检查 round_count 上限 + 4. increment round_count + 5. spawn 庞统 review + """ + if not self.dispatcher or not self.spawner: + return [] + + bb = Blackboard(db_path) + reviewed: List[str] = [] + + # 找所有 parent task(有子 task 的) + conn = get_connection(db_path) + try: + parent_rows = conn.execute( + "SELECT DISTINCT parent_task FROM tasks WHERE parent_task IS NOT NULL" + ).fetchall() + finally: + conn.close() + + for row in parent_rows: + parent_id = row["parent_task"] + try: + summary = bb.get_subtasks_summary(parent_id) + if not summary or not summary["all_terminal"]: + continue + + # 检查 parent 自身状态:只有 done 状态的 parent 才触发 + # (聚合后 parent 状态为 done 说明所有 sub 都完了) + if summary["parent_status"] != "done": + continue + + # 检查 round_count 上限 + if summary["round_count"] >= self.MAX_ROUNDS: + logger.warning( + "Parent %s reached max rounds (%d), skipping review", + parent_id, self.MAX_ROUNDS) + continue + + # 递增 round_count + new_round = bb.increment_round_count(parent_id) + + # 构建庞统 review 上下文 + outputs = bb.get_aggregate_outputs(parent_id) + comments = bb.get_round_comments(parent_id) + parent_task = bb.get_task(parent_id) + + if not parent_task: + continue + + # spawn 庞统 review + review_prompt = self._build_review_prompt( + parent_task, summary, outputs, comments, new_round + ) + + spawned = await self._spawn_pangtong_review( + parent_task, review_prompt, project_id + ) + if spawned: + reviewed.append(parent_id) + logger.info( + "Round %d review spawned for parent %s (subs: %s)", + new_round, parent_id, summary + ) + except Exception as e: + logger.exception("Round check error for parent %s", parent_id) + + return reviewed + + def _build_review_prompt(self, parent_task, summary: dict, + outputs: list, comments: list, + round_num: int) -> str: + """构建庞统 review prompt(§4.2 三问框架)""" + goal = parent_task.description or parent_task.title + must_haves = parent_task.must_haves or "{}" + + # 成果物摘要(限制 token) + output_lines = [] + for o in outputs[:20]: # 最多 20 个 + output_lines.append( + f"- [{o.get('task_title', '?')}] {o.get('title', '?')} " + f"({o.get('output_type', '?')}) by {o.get('agent', '?')}" + ) + + # 讨论摘要(限制 50 条) + comment_lines = [] + for c in comments[:50]: + comment_lines.append( + f"[{c.get('created_at', '?')[:16]}] {c.get('author', '?')}: {c.get('body', '?')[:200]}" + ) + + return f"""## 庞统 Review(第 {round_num} 轮) + +### Goal +{goal} + +### 验收标准 +{must_haves} + +### 本轮 Sub Task 状态 +- 完成: {summary['done']} +- 失败: {summary['failed']} +- 取消: {summary['cancelled']} +- 总计: {summary['total']} + +### 成果物 +{chr(10).join(output_lines) if output_lines else '无'} + +### 黑板讨论 +{chr(10).join(comment_lines) if comment_lines else '无'} + +### 三问 +1. Goal 还清晰吗?(是否有 goal drift) +2. 成果物覆盖 goal 了吗?(逐条检查验收标准) +3. 下一轮需要做什么?(创建新 sub tasks / 标记完成 / 调整方向) + +### 失败处理 +{f'有 {summary["failed"]} 个 sub task failed,优先判断是应该重试、换人、还是调整方案。' if summary['failed'] > 0 else '无失败'} + +### 你可以 +- 创建新一轮 sub tasks(通过 API: POST /api/projects/{{pid}}/tasks) +- 调整 goal(更新 parent task description/must_haves) +- 标记完成(如果 goal 已达成,回复 GOAL_ACHIEVED) + +Round 上限: {self.MAX_ROUNDS}(当前第 {round_num} 轮) +""" + + async def _spawn_pangtong_review(self, parent_task, + review_prompt: str, + project_id: str) -> bool: + """Spawn 庞统进行 review""" + try: + agent_id = "pangtong-fujunshi" + session_id = f"review-{parent_task.id}-r{parent_task.round_count + 1}" + + # 构建上下文 + context = self.spawner.build_context( + agent_id=agent_id, + task_id=parent_task.id, + project_id=project_id, + ) + + # 拼接 prompt + full_prompt = f"{context}\n\n{review_prompt}" + + # spawn + result = await self.spawner.spawn( + agent_id=agent_id, + session_id=session_id, + prompt=full_prompt, + project_id=project_id, + task_id=parent_task.id, + ) + + return result is not None + except Exception as e: + logger.exception("Failed to spawn pangtong review for %s", parent_task.id) + return False + def _advance_dependencies(self, db_path: Path) -> List[str]: """检查 blocked 任务,若所有依赖已完成则推进为 pending