goal_models.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. """
  2. Goal 数据模型与目标树操作。
  3. GoalTree 是 Runner 执行计划的可视化投影;Legacy 保留自动推进,
  4. Recursive 由协议审核流显式控制等待、待审核、失败和完成状态。
  5. """
  6. from dataclasses import dataclass, field
  7. from datetime import datetime
  8. from typing import Dict, Any, List, Optional, Literal
  9. import json
  10. # Goal 状态
  11. GoalStatus = Literal[
  12. "pending",
  13. "in_progress",
  14. "waiting_children",
  15. "pending_review",
  16. "completed",
  17. "failed",
  18. "abandoned",
  19. ]
  20. # Goal 类型
  21. GoalType = Literal["normal", "agent_call"]
  22. @dataclass
  23. class GoalStats:
  24. """目标统计信息"""
  25. message_count: int = 0 # 消息数量
  26. total_tokens: int = 0 # Token 总数
  27. total_cost: float = 0.0 # 总成本
  28. preview: Optional[str] = None # 工具调用摘要,如 "read_file → edit_file → bash"
  29. def to_dict(self) -> Dict[str, Any]:
  30. return {
  31. "message_count": self.message_count,
  32. "total_tokens": self.total_tokens,
  33. "total_cost": self.total_cost,
  34. "preview": self.preview,
  35. }
  36. @classmethod
  37. def from_dict(cls, data: Dict[str, Any]) -> "GoalStats":
  38. return cls(
  39. message_count=data.get("message_count", 0),
  40. total_tokens=data.get("total_tokens", 0),
  41. total_cost=data.get("total_cost", 0.0),
  42. preview=data.get("preview"),
  43. )
  44. @dataclass
  45. class Goal:
  46. """
  47. 执行目标
  48. 使用扁平列表 + parent_id 构建层级结构。
  49. agent_call 类型用于标记启动了 Sub-Trace 的 Goal。
  50. """
  51. id: str # 内部唯一 ID,纯自增("1", "2", "3"...)
  52. description: str # 目标描述
  53. reason: str = "" # 创建理由(为什么做)
  54. parent_id: Optional[str] = None # 父 Goal ID(层级关系)
  55. type: GoalType = "normal" # Goal 类型
  56. status: GoalStatus = "pending" # 状态
  57. summary: Optional[str] = None # 完成/放弃时的总结
  58. # agent_call 特有
  59. sub_trace_ids: Optional[List[Dict[str, str]]] = None # 启动的 Sub-Trace 信息 [{"trace_id": "...", "mission": "..."}]
  60. agent_call_mode: Optional[str] = None # "explore" | "delegate" | "sequential"
  61. sub_trace_metadata: Optional[Dict[str, Dict[str, Any]]] = None # Sub-Trace 元数据
  62. # 统计(后端维护,用于可视化边的数据)
  63. self_stats: GoalStats = field(default_factory=GoalStats) # 自身统计(仅直接关联的 messages)
  64. cumulative_stats: GoalStats = field(default_factory=GoalStats) # 累计统计(自身 + 所有后代)
  65. # 相关知识(自动检索注入)
  66. knowledge: Optional[List[Dict[str, Any]]] = None # 相关知识列表
  67. created_at: datetime = field(default_factory=datetime.now)
  68. def to_dict(self) -> Dict[str, Any]:
  69. """转换为字典"""
  70. return {
  71. "id": self.id,
  72. "description": self.description,
  73. "reason": self.reason,
  74. "parent_id": self.parent_id,
  75. "type": self.type,
  76. "status": self.status,
  77. "summary": self.summary,
  78. "sub_trace_ids": self.sub_trace_ids,
  79. "agent_call_mode": self.agent_call_mode,
  80. "sub_trace_metadata": self.sub_trace_metadata,
  81. "self_stats": self.self_stats.to_dict(),
  82. "cumulative_stats": self.cumulative_stats.to_dict(),
  83. "knowledge": self.knowledge,
  84. "created_at": self.created_at.isoformat() if self.created_at else None,
  85. }
  86. @classmethod
  87. def from_dict(cls, data: Dict[str, Any]) -> "Goal":
  88. """从字典创建"""
  89. created_at = data.get("created_at")
  90. if isinstance(created_at, str):
  91. created_at = datetime.fromisoformat(created_at)
  92. self_stats = data.get("self_stats", {})
  93. if isinstance(self_stats, dict):
  94. self_stats = GoalStats.from_dict(self_stats)
  95. cumulative_stats = data.get("cumulative_stats", {})
  96. if isinstance(cumulative_stats, dict):
  97. cumulative_stats = GoalStats.from_dict(cumulative_stats)
  98. return cls(
  99. id=data["id"],
  100. description=data["description"],
  101. reason=data.get("reason", ""),
  102. parent_id=data.get("parent_id"),
  103. type=data.get("type", "normal"),
  104. status=data.get("status", "pending"),
  105. summary=data.get("summary"),
  106. sub_trace_ids=data.get("sub_trace_ids"),
  107. agent_call_mode=data.get("agent_call_mode"),
  108. sub_trace_metadata=data.get("sub_trace_metadata"),
  109. self_stats=self_stats,
  110. cumulative_stats=cumulative_stats,
  111. knowledge=data.get("knowledge"),
  112. created_at=created_at or datetime.now(),
  113. )
  114. @dataclass
  115. class GoalTree:
  116. """
  117. 目标树 - 管理整个执行计划
  118. 使用扁平列表 + parent_id 构建层级结构。goal_tool 负责日常读写,
  119. Recursive 审核工具只把协议状态投影到这棵树,协议权威数据仍在 Trace context。
  120. """
  121. mission: str # 总任务描述
  122. goals: List[Goal] = field(default_factory=list) # 扁平列表(通过 parent_id 构建层级)
  123. current_id: Optional[str] = None # 当前焦点 goal ID
  124. _next_id: int = 1 # 内部 ID 计数器(私有字段)
  125. created_at: datetime = field(default_factory=datetime.now)
  126. def find(self, goal_id: str) -> Optional[Goal]:
  127. """按 ID 查找 Goal"""
  128. for goal in self.goals:
  129. if goal.id == goal_id:
  130. return goal
  131. return None
  132. def find_by_display_id(self, display_id: str) -> Optional[Goal]:
  133. """按显示 ID 查找 Goal(如 "1", "2.1", "2.2")"""
  134. for goal in self.goals:
  135. if self._generate_display_id(goal) == display_id:
  136. return goal
  137. return None
  138. def find_parent(self, goal_id: str) -> Optional[Goal]:
  139. """查找指定 Goal 的父节点"""
  140. goal = self.find(goal_id)
  141. if not goal or not goal.parent_id:
  142. return None
  143. return self.find(goal.parent_id)
  144. def get_children(self, parent_id: Optional[str]) -> List[Goal]:
  145. """获取指定父节点的所有子节点"""
  146. return [g for g in self.goals if g.parent_id == parent_id]
  147. def get_current(self) -> Optional[Goal]:
  148. """获取当前焦点 Goal"""
  149. if self.current_id:
  150. return self.find(self.current_id)
  151. return None
  152. def _generate_id(self) -> str:
  153. """生成新的 Goal ID(纯自增)"""
  154. new_id = str(self._next_id)
  155. self._next_id += 1
  156. return new_id
  157. def _generate_display_id(self, goal: Goal) -> str:
  158. """生成显示序号(1, 2, 2.1, 2.2...)"""
  159. if not goal.parent_id:
  160. # 顶层目标:找到在同级中的序号
  161. siblings = [g for g in self.goals if g.parent_id is None and g.status != "abandoned"]
  162. try:
  163. index = [g.id for g in siblings].index(goal.id) + 1
  164. return str(index)
  165. except ValueError:
  166. return "?"
  167. else:
  168. # 子目标:父序号 + "." + 在同级中的序号
  169. parent = self.find(goal.parent_id)
  170. if not parent:
  171. return "?"
  172. parent_display = self._generate_display_id(parent)
  173. siblings = [g for g in self.goals if g.parent_id == goal.parent_id and g.status != "abandoned"]
  174. try:
  175. index = [g.id for g in siblings].index(goal.id) + 1
  176. return f"{parent_display}.{index}"
  177. except ValueError:
  178. return f"{parent_display}.?"
  179. def _find_next_pending_goal(self, completed_goal_id: str) -> Optional[Goal]:
  180. """
  181. 完成 goal 后,自动查找下一个应该执行的 pending goal
  182. 查找顺序:
  183. 1. 同级的下一个 pending goal
  184. 2. 父级的下一个 pending goal
  185. 3. 任意顶层 pending goal
  186. Args:
  187. completed_goal_id: 刚完成的 goal ID
  188. Returns:
  189. 下一个 pending goal,如果没有则返回 None
  190. """
  191. completed_goal = self.find(completed_goal_id)
  192. if not completed_goal:
  193. return None
  194. # 1. 查找同级的下一个 pending goal
  195. siblings = self.get_children(completed_goal.parent_id)
  196. found_current = False
  197. for sibling in siblings:
  198. if sibling.id == completed_goal_id:
  199. found_current = True
  200. continue
  201. if found_current and sibling.status == "pending":
  202. return sibling
  203. # 2. 如果有父级,查找父级的下一个 pending goal
  204. if completed_goal.parent_id:
  205. return self._find_next_pending_goal(completed_goal.parent_id)
  206. # 3. 查找任意顶层 pending goal
  207. for goal in self.goals:
  208. if goal.parent_id is None and goal.status == "pending":
  209. return goal
  210. return None
  211. def add_goals(
  212. self,
  213. descriptions: List[str],
  214. reasons: Optional[List[str]] = None,
  215. parent_id: Optional[str] = None
  216. ) -> List[Goal]:
  217. """
  218. 添加目标
  219. 如果 parent_id 为 None,添加到顶层
  220. 如果 parent_id 有值,添加为该 goal 的子目标
  221. """
  222. if parent_id:
  223. parent = self.find(parent_id)
  224. if not parent:
  225. raise ValueError(f"Parent goal not found: {parent_id}")
  226. # 创建新目标
  227. new_goals = []
  228. for i, desc in enumerate(descriptions):
  229. goal_id = self._generate_id()
  230. reason = reasons[i] if reasons and i < len(reasons) else ""
  231. goal = Goal(
  232. id=goal_id,
  233. description=desc.strip(),
  234. reason=reason,
  235. parent_id=parent_id
  236. )
  237. self.goals.append(goal)
  238. new_goals.append(goal)
  239. return new_goals
  240. def add_goals_after(
  241. self,
  242. target_id: str,
  243. descriptions: List[str],
  244. reasons: Optional[List[str]] = None
  245. ) -> List[Goal]:
  246. """
  247. 在指定 Goal 后面添加兄弟节点
  248. 新创建的 goals 与 target 有相同的 parent_id,
  249. 并插入到 goals 列表中 target 的后面。
  250. """
  251. target = self.find(target_id)
  252. if not target:
  253. raise ValueError(f"Target goal not found: {target_id}")
  254. # 创建新 goals(parent_id 与 target 相同)
  255. new_goals = []
  256. for i, desc in enumerate(descriptions):
  257. goal_id = self._generate_id()
  258. reason = reasons[i] if reasons and i < len(reasons) else ""
  259. goal = Goal(
  260. id=goal_id,
  261. description=desc.strip(),
  262. reason=reason,
  263. parent_id=target.parent_id # 同层级
  264. )
  265. new_goals.append(goal)
  266. # 插入到 target 后面(调整 goals 列表顺序)
  267. target_index = self.goals.index(target)
  268. for i, goal in enumerate(new_goals):
  269. self.goals.insert(target_index + 1 + i, goal)
  270. return new_goals
  271. def focus(self, goal_id: str) -> Goal:
  272. """切换焦点到指定 Goal,并将其状态设为 in_progress"""
  273. goal = self.find(goal_id)
  274. if not goal:
  275. raise ValueError(f"Goal not found: {goal_id}")
  276. # 更新状态
  277. if goal.status == "pending":
  278. goal.status = "in_progress"
  279. self.current_id = goal_id
  280. return goal
  281. def complete(
  282. self,
  283. goal_id: str,
  284. summary: str,
  285. clear_focus: bool = True,
  286. *,
  287. auto_advance: bool = True,
  288. cascade_parent: bool = True,
  289. ) -> Goal:
  290. """
  291. 完成指定 Goal,并按开关决定是否自动切换兄弟或级联完成父目标。
  292. goal_tool 在 Legacy 中保持两项旧行为;Recursive 关闭它们,将焦点交回直接父 Goal 由当前 Agent 重新决策。
  293. Args:
  294. goal_id: 要完成的目标 ID
  295. summary: 完成总结
  296. clear_focus: 如果完成的是当前焦点,是否清除焦点(默认 True)
  297. """
  298. goal = self.find(goal_id)
  299. if not goal:
  300. raise ValueError(f"Goal not found: {goal_id}")
  301. goal.status = "completed"
  302. goal.summary = summary
  303. # 如果完成的是当前焦点,根据参数决定是否清除焦点
  304. if clear_focus and self.current_id == goal_id:
  305. # 不直接清空,尝试自动切换到下一个 pending goal
  306. next_goal = self._find_next_pending_goal(goal_id) if auto_advance else None
  307. if next_goal:
  308. self.current_id = next_goal.id
  309. elif not auto_advance and goal.parent_id:
  310. self.current_id = goal.parent_id
  311. else:
  312. self.current_id = None
  313. # 检查是否所有兄弟都完成了,如果是则自动完成父节点
  314. if cascade_parent and goal.parent_id:
  315. siblings = self.get_children(goal.parent_id)
  316. all_completed = all(g.status == "completed" for g in siblings)
  317. if all_completed:
  318. parent = self.find(goal.parent_id)
  319. if parent and parent.status != "completed":
  320. # 自动级联完成父节点
  321. parent.status = "completed"
  322. if not parent.summary:
  323. parent.summary = "所有子目标已完成"
  324. return goal
  325. def abandon(self, goal_id: str, reason: str, *, auto_advance: bool = True) -> Goal:
  326. """放弃指定 Goal"""
  327. goal = self.find(goal_id)
  328. if not goal:
  329. raise ValueError(f"Goal not found: {goal_id}")
  330. goal.status = "abandoned"
  331. goal.summary = reason
  332. # 如果放弃的是当前焦点,尝试自动切换到下一个 pending goal
  333. if self.current_id == goal_id:
  334. next_goal = self._find_next_pending_goal(goal_id) if auto_advance else None
  335. if next_goal:
  336. self.current_id = next_goal.id
  337. else:
  338. self.current_id = None
  339. return goal
  340. def fail(self, goal_id: str, reason: str) -> Goal:
  341. """将 Goal 标记为失败,不级联父级也不自动切换兄弟。
  342. 该方法供直接 GoalTree 调用者使用;TaskReview FAIL 的持久化路径通过 TraceStore.update_goal() 执行同样语义。"""
  343. goal = self.find(goal_id)
  344. if not goal:
  345. raise ValueError(f"Goal not found: {goal_id}")
  346. goal.status = "failed"
  347. goal.summary = reason
  348. if self.current_id == goal_id:
  349. self.current_id = goal.parent_id
  350. return goal
  351. def to_prompt(self, include_abandoned: bool = False, include_summary: bool = False) -> str:
  352. """
  353. 格式化为 Prompt 注入文本
  354. Args:
  355. include_abandoned: 是否包含已废弃的目标
  356. include_summary: 是否显示 completed/abandoned goals 的 summary 详情
  357. False(默认)= 精简视图,用于日常周期性注入
  358. True = 完整视图(含 summary),用于压缩时提供上下文
  359. 展示策略:
  360. - 过滤掉 abandoned 目标(除非明确要求)
  361. - 完整展示所有顶层目标
  362. - 完整展示当前 focus 目标的父链及其所有子孙
  363. - 其他分支的子目标折叠显示(只显示数量和状态)
  364. - include_summary=True 时不折叠,全部展开并显示 summary
  365. """
  366. lines = []
  367. lines.append(f"**Mission**: {self.mission}")
  368. if self.current_id:
  369. current = self.find(self.current_id)
  370. if current:
  371. display_id = self._generate_display_id(current)
  372. lines.append(f"**Current**: {display_id} {current.description}")
  373. lines.append("")
  374. lines.append("**Progress**:")
  375. # 获取当前焦点的祖先链(从根到当前节点的路径)
  376. current_path = set()
  377. if self.current_id:
  378. goal = self.find(self.current_id)
  379. while goal:
  380. current_path.add(goal.id)
  381. if goal.parent_id:
  382. goal = self.find(goal.parent_id)
  383. else:
  384. break
  385. def format_goal(goal: Goal, indent: int = 0) -> List[str]:
  386. # 跳过废弃的目标(除非明确要求包含)
  387. if goal.status == "abandoned" and not include_abandoned:
  388. return []
  389. prefix = " " * indent
  390. # 状态图标
  391. if goal.status == "completed":
  392. icon = "[✓]"
  393. elif goal.status in {"in_progress", "waiting_children", "pending_review"}:
  394. icon = "[→]"
  395. elif goal.status in {"failed", "abandoned"}:
  396. icon = "[✗]"
  397. else:
  398. icon = "[ ]"
  399. # 生成显示序号
  400. display_id = self._generate_display_id(goal)
  401. # 当前焦点标记
  402. current_mark = " ← current" if goal.id == self.current_id else ""
  403. result = [f"{prefix}{icon} {display_id}. {goal.description}{current_mark}"]
  404. # 显示 summary:include_summary=True 时全部显示,否则只在焦点路径上显示
  405. if goal.summary and (include_summary or goal.id in current_path):
  406. result.append(f"{prefix} → {goal.summary}")
  407. # 显示相关知识:仅在当前焦点 goal 显示
  408. if goal.id == self.current_id and goal.knowledge:
  409. result.append(f"{prefix} 📚 相关知识 ({len(goal.knowledge)} 条):")
  410. for idx, k in enumerate(goal.knowledge[:3], 1):
  411. k_id = k.get('id', 'N/A')
  412. k_content = k.get('content', '').strip()
  413. result.append(f"{prefix} {idx}. [{k_id}] {k_content}")
  414. # 递归处理子目标
  415. children = self.get_children(goal.id)
  416. # include_summary 模式下不折叠,全部展开
  417. if include_summary:
  418. for child in children:
  419. result.extend(format_goal(child, indent + 1))
  420. return result
  421. # 判断是否需要折叠
  422. # 如果当前 goal 或其子孙在焦点路径上,完整展示
  423. should_expand = goal.id in current_path or any(
  424. child.id in current_path for child in self._get_all_descendants(goal.id)
  425. )
  426. if should_expand or not children:
  427. # 完整展示子目标
  428. for child in children:
  429. result.extend(format_goal(child, indent + 1))
  430. else:
  431. # 折叠显示:只显示子目标的统计
  432. non_abandoned = [c for c in children if c.status != "abandoned"]
  433. if non_abandoned:
  434. completed = sum(1 for c in non_abandoned if c.status == "completed")
  435. in_progress = sum(1 for c in non_abandoned if c.status == "in_progress")
  436. pending = sum(1 for c in non_abandoned if c.status == "pending")
  437. status_parts = []
  438. if completed > 0:
  439. status_parts.append(f"{completed} completed")
  440. if in_progress > 0:
  441. status_parts.append(f"{in_progress} in progress")
  442. if pending > 0:
  443. status_parts.append(f"{pending} pending")
  444. status_str = ", ".join(status_parts)
  445. result.append(f"{prefix} ({len(non_abandoned)} subtasks: {status_str})")
  446. return result
  447. # 处理所有顶层目标
  448. top_goals = self.get_children(None)
  449. for goal in top_goals:
  450. lines.extend(format_goal(goal))
  451. return "\n".join(lines)
  452. def _get_all_descendants(self, goal_id: str) -> List[Goal]:
  453. """获取指定 Goal 的所有子孙节点"""
  454. descendants = []
  455. children = self.get_children(goal_id)
  456. for child in children:
  457. descendants.append(child)
  458. descendants.extend(self._get_all_descendants(child.id))
  459. return descendants
  460. def to_dict(self) -> Dict[str, Any]:
  461. """转换为字典"""
  462. return {
  463. "mission": self.mission,
  464. "goals": [g.to_dict() for g in self.goals],
  465. "current_id": self.current_id,
  466. "_next_id": self._next_id,
  467. "created_at": self.created_at.isoformat() if self.created_at else None,
  468. }
  469. @classmethod
  470. def from_dict(cls, data: Dict[str, Any]) -> "GoalTree":
  471. """从字典创建"""
  472. goals = [Goal.from_dict(g) for g in data.get("goals", [])]
  473. created_at = data.get("created_at")
  474. if isinstance(created_at, str):
  475. created_at = datetime.fromisoformat(created_at)
  476. return cls(
  477. mission=data["mission"],
  478. goals=goals,
  479. current_id=data.get("current_id"),
  480. _next_id=data.get("_next_id", 1),
  481. created_at=created_at or datetime.now(),
  482. )
  483. def rebuild_for_rewind(self, cutoff_time: datetime) -> "GoalTree":
  484. """
  485. 为 Rewind 重建干净的 GoalTree,移除截断点之后的目标并重置未完成状态。
  486. AgentRunner 的 rewind 流程调用它;Recursive 的 waiting_children/pending_review 也会回到 pending。
  487. 以截断点消息的 created_at 为界:
  488. - 保留 created_at <= cutoff_time 的所有 goals(无论状态)
  489. - 丢弃 cutoff_time 之后创建的 goals
  490. - 将被保留的 in_progress goals 重置为 pending
  491. - 清空 current_id,让 Agent 重新选择焦点
  492. Args:
  493. cutoff_time: 截断点消息的创建时间
  494. Returns:
  495. 新的干净 GoalTree
  496. """
  497. surviving_goals = []
  498. for goal in self.goals:
  499. if goal.created_at <= cutoff_time:
  500. surviving_goals.append(goal)
  501. # 清理 parent_id 引用:如果 parent 不在存活列表中,设为 None
  502. surviving_ids = {g.id for g in surviving_goals}
  503. for goal in surviving_goals:
  504. if goal.parent_id and goal.parent_id not in surviving_ids:
  505. goal.parent_id = None
  506. # 将 in_progress 重置为 pending(回溯后需要重新执行)
  507. if goal.status in {"in_progress", "waiting_children", "pending_review"}:
  508. goal.status = "pending"
  509. new_tree = GoalTree(
  510. mission=self.mission,
  511. goals=surviving_goals,
  512. current_id=None,
  513. _next_id=self._next_id,
  514. created_at=self.created_at,
  515. )
  516. return new_tree
  517. def save(self, path: str) -> None:
  518. """保存到 JSON 文件"""
  519. with open(path, "w", encoding="utf-8") as f:
  520. json.dump(self.to_dict(), f, ensure_ascii=False, indent=2)
  521. @classmethod
  522. def load(cls, path: str) -> "GoalTree":
  523. """从 JSON 文件加载"""
  524. with open(path, "r", encoding="utf-8") as f:
  525. data = json.load(f)
  526. return cls.from_dict(data)