goal_models.py 20 KB

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