tool.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. """
  2. Goal 工具 - 计划管理
  3. 提供 goal 工具供 LLM 管理执行计划。
  4. """
  5. from typing import Optional, List, TYPE_CHECKING
  6. if TYPE_CHECKING:
  7. from agent.goal.models import GoalTree
  8. from agent.execution.protocols import TraceStore
  9. async def goal_tool(
  10. tree: "GoalTree",
  11. store: Optional["TraceStore"] = None,
  12. trace_id: Optional[str] = None,
  13. add: Optional[str] = None,
  14. reason: Optional[str] = None,
  15. after: Optional[str] = None,
  16. under: Optional[str] = None,
  17. done: Optional[str] = None,
  18. abandon: Optional[str] = None,
  19. focus: Optional[str] = None,
  20. ) -> str:
  21. """
  22. 管理执行计划。
  23. Args:
  24. tree: GoalTree 实例
  25. store: TraceStore 实例(用于推送事件)
  26. trace_id: 当前 Trace ID
  27. add: 添加目标(逗号分隔多个)
  28. reason: 创建理由(逗号分隔多个,与 add 一一对应)
  29. after: 在指定目标后面添加(同层级)
  30. under: 为指定目标添加子目标
  31. done: 完成当前目标,值为 summary
  32. abandon: 放弃当前目标,值为原因
  33. focus: 切换焦点到指定 ID
  34. Returns:
  35. 更新后的计划状态文本
  36. """
  37. changes = []
  38. # 1. 处理 done(完成当前目标)
  39. if done is not None:
  40. if not tree.current_id:
  41. return f"错误:没有当前目标可以完成。当前焦点为空,请先使用 focus 参数切换到要完成的目标。\n\n当前计划:\n{tree.to_prompt()}"
  42. # 完成当前目标
  43. # 如果同时指定了 focus,则不清空焦点(后面会切换到新目标)
  44. # 如果只有 done,则清空焦点
  45. clear_focus = (focus is None)
  46. goal = tree.complete(tree.current_id, done, clear_focus=clear_focus)
  47. display_id = tree._generate_display_id(goal)
  48. changes.append(f"已完成: {display_id}. {goal.description}")
  49. # 推送事件
  50. if store and trace_id:
  51. print(f"[DEBUG] goal_tool: calling store.update_goal for done: goal_id={goal.id}")
  52. await store.update_goal(trace_id, goal.id, status="completed", summary=done)
  53. else:
  54. print(f"[DEBUG] goal_tool: skip event push (store={store}, trace_id={trace_id})")
  55. # 检查是否有级联完成的父目标(complete方法已经处理,这里只需要记录)
  56. if goal.parent_id:
  57. parent = tree.find(goal.parent_id)
  58. if parent and parent.status == "completed":
  59. parent_display_id = tree._generate_display_id(parent)
  60. changes.append(f"自动完成: {parent_display_id}. {parent.description}(所有子目标已完成)")
  61. # 2. 处理 focus(切换焦点到新目标)
  62. if focus is not None:
  63. goal = tree.find_by_display_id(focus)
  64. if not goal:
  65. return f"错误:找不到目标 {focus}\n\n当前计划:\n{tree.to_prompt()}"
  66. tree.focus(goal.id)
  67. display_id = tree._generate_display_id(goal)
  68. changes.append(f"切换焦点: {display_id}. {goal.description}")
  69. # 3. 处理 abandon(放弃当前目标)
  70. if abandon is not None:
  71. if not tree.current_id:
  72. return f"错误:没有当前目标可以放弃。当前焦点为空。\n\n当前计划:\n{tree.to_prompt()}"
  73. goal = tree.abandon(tree.current_id, abandon)
  74. display_id = tree._generate_display_id(goal)
  75. changes.append(f"已放弃: {display_id}. {goal.description}")
  76. # 推送事件
  77. if store and trace_id:
  78. print(f"[DEBUG] goal_tool: calling store.update_goal for abandon: goal_id={goal.id}")
  79. await store.update_goal(trace_id, goal.id, status="abandoned", summary=abandon)
  80. else:
  81. print(f"[DEBUG] goal_tool: skip event push (store={store}, trace_id={trace_id})")
  82. # 4. 处理 add
  83. if add is not None:
  84. # 检查 after 和 under 互斥
  85. if after is not None and under is not None:
  86. return "错误:after 和 under 参数不能同时指定"
  87. descriptions = [d.strip() for d in add.split(",") if d.strip()]
  88. if descriptions:
  89. # 解析 reasons(与 descriptions 一一对应)
  90. reasons = None
  91. if reason:
  92. reasons = [r.strip() for r in reason.split(",")]
  93. # 如果 reasons 数量少于 descriptions,补空字符串
  94. while len(reasons) < len(descriptions):
  95. reasons.append("")
  96. # 确定添加位置
  97. if after is not None:
  98. # 在指定 goal 后面添加(同层级)
  99. target_goal = tree.find_by_display_id(after)
  100. if not target_goal:
  101. return f"错误:找不到目标 {after}\n\n当前计划:\n{tree.to_prompt()}"
  102. new_goals = tree.add_goals_after(target_goal.id, descriptions, reasons=reasons)
  103. changes.append(f"在 {tree._generate_display_id(target_goal)} 后面添加 {len(new_goals)} 个同级目标")
  104. elif under is not None:
  105. # 为指定 goal 添加子目标
  106. parent_goal = tree.find_by_display_id(under)
  107. if not parent_goal:
  108. return f"错误:找不到目标 {under}\n\n当前计划:\n{tree.to_prompt()}"
  109. new_goals = tree.add_goals(descriptions, reasons=reasons, parent_id=parent_goal.id)
  110. changes.append(f"在 {tree._generate_display_id(parent_goal)} 下添加 {len(new_goals)} 个子目标")
  111. else:
  112. # 默认行为:添加到当前焦点下(如果有焦点),否则添加到顶层
  113. parent_id = tree.current_id
  114. new_goals = tree.add_goals(descriptions, reasons=reasons, parent_id=parent_id)
  115. if parent_id:
  116. parent_display_id = tree._generate_display_id(tree.find(parent_id))
  117. changes.append(f"在 {parent_display_id} 下添加 {len(new_goals)} 个子目标")
  118. else:
  119. changes.append(f"添加 {len(new_goals)} 个顶层目标")
  120. # 推送事件
  121. if store and trace_id:
  122. print(f"[DEBUG] goal_tool: calling store.add_goal for {len(new_goals)} new goals")
  123. for goal in new_goals:
  124. await store.add_goal(trace_id, goal)
  125. else:
  126. print(f"[DEBUG] goal_tool: skip event push (store={store}, trace_id={trace_id})")
  127. # 如果没有焦点且添加了目标,自动 focus 到第一个新目标
  128. if not tree.current_id and new_goals:
  129. tree.focus(new_goals[0].id)
  130. display_id = tree._generate_display_id(new_goals[0])
  131. changes.append(f"自动切换焦点: {display_id}")
  132. # 返回当前状态
  133. result = []
  134. if changes:
  135. result.append("## 更新")
  136. result.extend(f"- {c}" for c in changes)
  137. result.append("")
  138. result.append("## Current Plan")
  139. result.append(tree.to_prompt())
  140. return "\n".join(result)
  141. def create_goal_tool_schema() -> dict:
  142. """创建 goal 工具的 JSON Schema"""
  143. return {
  144. "name": "goal",
  145. "description": """管理执行计划。
  146. - add: 添加目标(逗号分隔多个)
  147. - reason: 创建理由(逗号分隔多个,与 add 一一对应)。说明为什么要做这些目标。
  148. - after: 在指定目标后面添加(同层级)。使用目标的 ID。
  149. - under: 为指定目标添加子目标。使用目标的 ID。如已有子目标,追加到最后。
  150. - done: 完成当前目标,值为 summary
  151. - abandon: 放弃当前目标,值为原因(会触发 context 压缩)
  152. - focus: 切换焦点到指定目标。使用目标的 ID。
  153. 位置控制(优先使用 after):
  154. - 不指定 after/under: 添加到当前 focus 下作为子目标(无 focus 时添加到顶层)
  155. - after="X": 在目标 X 后面添加兄弟节点(同层级)
  156. - under="X": 为目标 X 添加子目标
  157. - after 和 under 不能同时指定
  158. 执行顺序:
  159. - done → focus → abandon → add
  160. - 如果同时指定 done 和 focus,会先完成当前目标,再切换焦点到新目标
  161. 示例:
  162. - goal(add="分析代码, 实现功能, 测试") - 添加顶层目标
  163. - goal(add="设计接口, 实现代码", under="2") - 为目标2添加子目标
  164. - goal(add="编写文档", after="3") - 在目标3后面添加同级任务
  165. - goal(add="集成测试", after="2.2") - 在目标2.2后面添加同级任务
  166. - goal(done="发现用户模型在 models/user.py") - 完成当前目标
  167. - goal(done="已完成调研", focus="2") - 完成当前目标,切换到目标2
  168. - goal(abandon="方案A需要Redis,环境没有") - 放弃当前目标
  169. 注意:
  170. - 目标 ID 的格式为 "1", "2", "2.1", "2.2" 等,在计划视图中可以看到
  171. - reason 应该与 add 的目标数量一致,如果数量不一致,缺少的 reason 将为空
  172. """,
  173. "parameters": {
  174. "type": "object",
  175. "properties": {
  176. "add": {
  177. "type": "string",
  178. "description": "添加目标(逗号分隔多个)"
  179. },
  180. "reason": {
  181. "type": "string",
  182. "description": "创建理由(逗号分隔多个,与 add 一一对应)。说明为什么要做这些目标。"
  183. },
  184. "after": {
  185. "type": "string",
  186. "description": "在指定目标后面添加(同层级)。使用目标的 ID,如 \"2\" 或 \"2.1\"。"
  187. },
  188. "under": {
  189. "type": "string",
  190. "description": "为指定目标添加子目标。使用目标的 ID,如 \"2\" 或 \"2.1\"。"
  191. },
  192. "done": {
  193. "type": "string",
  194. "description": "完成当前目标,值为 summary"
  195. },
  196. "abandon": {
  197. "type": "string",
  198. "description": "放弃当前目标,值为原因"
  199. },
  200. "focus": {
  201. "type": "string",
  202. "description": "切换焦点到指定目标。使用目标的 ID,如 \"2\" 或 \"2.1\"。"
  203. }
  204. },
  205. "required": []
  206. }
  207. }