goal_tool.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. """
  2. Goal 工具与目标树持久化入口。
  3. ToolRegistry 向 LLM 暴露 goal(),实际变更由 goal_tool() 执行;
  4. Recursive 还会在此阻止绕过待审核或已批准下一步的 Goal 修改。
  5. """
  6. import logging
  7. from typing import Optional, List, TYPE_CHECKING
  8. from cyber_agent.tools import tool
  9. from cyber_agent.core.agent_mode import policy_from_context
  10. from cyber_agent.core.task_protocol import ensure_task_protocol
  11. if TYPE_CHECKING:
  12. from .goal_models import GoalTree, Goal
  13. from .protocols import TraceStore
  14. logger = logging.getLogger(__name__)
  15. # ===== 知识注入 =====
  16. async def inject_knowledge_for_goal(
  17. goal: "Goal",
  18. tree: "GoalTree",
  19. store: Optional["TraceStore"] = None,
  20. trace_id: Optional[str] = None,
  21. knowledge_config: Optional[dict] = None,
  22. sequence: Optional[int] = None,
  23. ) -> Optional[str]:
  24. """
  25. 为指定 goal 注入相关知识。
  26. Args:
  27. goal: 目标对象
  28. tree: GoalTree
  29. store: TraceStore(用于持久化)
  30. trace_id: Trace ID
  31. knowledge_config: 知识管理配置(KnowledgeConfig 对象)
  32. sequence: 当前消息序列号(用于记录注入时机)
  33. Returns:
  34. 注入结果描述(如 "📚 已注入 3 条相关知识"),无结果返回 None
  35. """
  36. # 检查是否启用知识注入
  37. if knowledge_config and not getattr(knowledge_config, 'enable_injection', True):
  38. logger.debug(f"[Knowledge Inject] 知识注入已禁用,跳过")
  39. return None
  40. # Goal focus 过去直接调用 ``_run_remote_agent``,会绕过 agent()
  41. # 已实施的 Structured Recursive 远端门禁。这里必须从当前
  42. # Trace 恢复权威策略;读取失败时不能猜测为 Legacy 继续出网。
  43. if store is not None or trace_id is not None:
  44. if store is None or not trace_id:
  45. logger.warning("[Knowledge Inject] Trace context incomplete; skip remote injection")
  46. goal.knowledge = []
  47. return None
  48. try:
  49. trace = await store.get_trace(trace_id)
  50. if trace is None:
  51. logger.warning(
  52. f"[Knowledge Inject] Trace not found: {trace_id}; skip remote injection"
  53. )
  54. goal.knowledge = []
  55. return None
  56. policy = policy_from_context(trace.context)
  57. except Exception as exc:
  58. logger.warning(
  59. f"[Knowledge Inject] Cannot restore Trace policy; skip remote injection: {exc}"
  60. )
  61. goal.knowledge = []
  62. return None
  63. if policy.requires_task_protocol:
  64. logger.info(
  65. "[Knowledge Inject] Structured Recursive forbids remote Librarian; skip"
  66. )
  67. goal.knowledge = []
  68. return None
  69. try:
  70. import re
  71. from cyber_agent.tools.builtin.subagent import _run_remote_agent
  72. logger.info(f"[Knowledge Inject] goal: {goal.id}, query: {goal.description[:80]}")
  73. # 通过统一 agent 工具调用远端 Librarian,明确走 ask 策略
  74. result = await _run_remote_agent(
  75. agent_type="remote_librarian",
  76. task=goal.description,
  77. messages=None,
  78. continue_from=None, # 知识注入每次独立查询,不续跑
  79. skills=["ask_strategy"],
  80. )
  81. if result.get("status") != "completed":
  82. logger.warning(f"[Knowledge Inject] 远端 Librarian 调用失败: {result.get('error')}")
  83. goal.knowledge = []
  84. return None
  85. response_text = result.get("summary", "")
  86. # 从 summary 中解析 source_ids(Librarian prompt 约定以 knowledge-xxx 形式引用)
  87. source_ids = re.findall(r'\[?(knowledge-[a-zA-Z0-9_-]+)\]?', response_text)
  88. source_ids = list(dict.fromkeys(source_ids)) # 去重保序
  89. if source_ids:
  90. goal.knowledge = [{"id": sid} for sid in source_ids]
  91. knowledge_count = len(source_ids)
  92. logger.info(f"[Knowledge Inject] 注入 {knowledge_count} 条知识到 goal {goal.id}")
  93. if store and trace_id:
  94. await store.update_goal_tree(trace_id, tree)
  95. if sequence is not None:
  96. await store.append_cognition_event(
  97. trace_id=trace_id,
  98. event={
  99. "type": "query",
  100. "sequence": sequence,
  101. "goal_id": goal.id,
  102. "query": goal.description,
  103. "response": response_text[:2000],
  104. "source_ids": source_ids,
  105. "sources": [],
  106. }
  107. )
  108. logger.info(f"[Knowledge Inject] 已记录 query 事件到 cognition_log")
  109. return f"📚 已注入 {knowledge_count} 条相关知识"
  110. else:
  111. goal.knowledge = []
  112. logger.info(f"[Knowledge Inject] 未找到相关知识")
  113. return None
  114. except Exception as e:
  115. logger.warning(f"[Knowledge Inject] 知识注入失败: {e}")
  116. goal.knowledge = []
  117. return None
  118. # ===== LLM 可调用的 goal 工具 =====
  119. @tool(description="管理执行计划,添加/完成/放弃目标,切换焦点", hidden_params=["context"], groups=["core"])
  120. async def goal(
  121. add: Optional[str] = None,
  122. reason: Optional[str] = None,
  123. after: Optional[str] = None,
  124. under: Optional[str] = None,
  125. done: Optional[str] = None,
  126. abandon: Optional[str] = None,
  127. focus: Optional[str] = None,
  128. context: Optional[dict] = None
  129. ) -> str:
  130. """
  131. LLM 可调用的计划管理工具,由 ToolRegistry 注入当前 GoalTree 和 Trace 上下文。
  132. 它只解析公开参数,再转交 goal_tool() 持久化状态并发送前端事件。
  133. Args:
  134. add: 添加目标(逗号分隔多个)
  135. reason: 创建理由(逗号分隔多个,与 add 一一对应)
  136. after: 在指定目标后面添加(同层级)
  137. under: 为指定目标添加子目标
  138. done: 完成当前目标,值为 summary
  139. abandon: 放弃当前目标,值为原因
  140. focus: 切换焦点到指定 ID
  141. context: 工具执行上下文(包含 store、trace_id、goal_tree)
  142. Returns:
  143. str: 更新后的计划状态文本
  144. """
  145. # GoalTree 从 context 获取,每个 agent 实例独立,不再依赖全局变量
  146. tree = context.get("goal_tree") if context else None
  147. if tree is None:
  148. return "错误:GoalTree 未初始化"
  149. # 从 context 获取 store、trace_id 和 knowledge_config
  150. store = context.get("store") if context else None
  151. trace_id = context.get("trace_id") if context else None
  152. knowledge_config = context.get("knowledge_config") if context else None
  153. return await goal_tool(
  154. tree=tree,
  155. store=store,
  156. trace_id=trace_id,
  157. add=add,
  158. reason=reason,
  159. after=after,
  160. under=under,
  161. done=done,
  162. abandon=abandon,
  163. focus=focus,
  164. knowledge_config=knowledge_config,
  165. context=context
  166. )
  167. # ===== 核心逻辑函数 =====
  168. async def goal_tool(
  169. tree: "GoalTree",
  170. store: Optional["TraceStore"] = None,
  171. trace_id: Optional[str] = None,
  172. add: Optional[str] = None,
  173. reason: Optional[str] = None,
  174. after: Optional[str] = None,
  175. under: Optional[str] = None,
  176. done: Optional[str] = None,
  177. abandon: Optional[str] = None,
  178. focus: Optional[str] = None,
  179. knowledge_config: Optional[object] = None,
  180. context: Optional[dict] = None,
  181. ) -> str:
  182. """
  183. Goal 工具的共用实现,更新 GoalTree、TraceStore 和实时事件。
  184. Recursive 会先读持久化协议状态,关闭自动父级完成与兄弟切换,把后续决策留给 TaskReview。
  185. Args:
  186. tree: GoalTree 实例
  187. store: TraceStore 实例(用于推送事件)
  188. trace_id: 当前 Trace ID
  189. add: 添加目标(逗号分隔多个)
  190. reason: 创建理由(逗号分隔多个,与 add 一一对应)
  191. after: 在指定目标后面添加(同层级)
  192. under: 为指定目标添加子目标
  193. done: 完成当前目标,值为 summary
  194. abandon: 放弃当前目标,值为原因
  195. focus: 切换焦点到指定 ID
  196. knowledge_config: 知识管理配置(KnowledgeConfig 对象)
  197. Returns:
  198. 更新后的计划状态文本
  199. """
  200. changes = []
  201. recursive_protocol = False
  202. if store and trace_id:
  203. trace = await store.get_trace(trace_id)
  204. if trace:
  205. recursive_protocol = policy_from_context(
  206. trace.context
  207. ).requires_task_protocol
  208. if recursive_protocol:
  209. state = ensure_task_protocol(trace.context)
  210. has_goal_mutation = any(
  211. value is not None
  212. for value in (add, done, abandon, focus)
  213. )
  214. if state["pending_reviews"] and has_goal_mutation:
  215. return (
  216. "错误:当前有子任务报告待审核,只允许调用 "
  217. "review_task_result。"
  218. )
  219. if state["next_actions"] and has_goal_mutation:
  220. return (
  221. "错误:当前有已批准的下一步待执行,只允许调用 "
  222. "agent 执行该 action。"
  223. )
  224. # 1. 处理 done(完成当前目标)
  225. if done is not None:
  226. if not tree.current_id:
  227. return f"错误:没有当前目标可以完成。当前焦点为空,请先使用 focus 参数切换到要完成的目标。\n\n当前计划:\n{tree.to_prompt()}"
  228. # 完成当前目标
  229. # 如果同时指定了 focus,则不清空焦点(后面会切换到新目标)
  230. # 如果只有 done,则清空焦点
  231. clear_focus = (focus is None)
  232. goal = tree.complete(
  233. tree.current_id,
  234. done,
  235. clear_focus=clear_focus,
  236. auto_advance=not recursive_protocol,
  237. cascade_parent=not recursive_protocol,
  238. )
  239. display_id = tree._generate_display_id(goal)
  240. changes.append(f"已完成: {display_id}. {goal.description}")
  241. # 推送事件
  242. if store and trace_id:
  243. await store.update_goal(
  244. trace_id,
  245. goal.id,
  246. cascade_completion=not recursive_protocol,
  247. status="completed",
  248. summary=done,
  249. )
  250. # 检查是否有级联完成的父目标(complete方法已经处理,这里只需要记录)
  251. if goal.parent_id and not recursive_protocol:
  252. parent = tree.find(goal.parent_id)
  253. if parent and parent.status == "completed":
  254. parent_display_id = tree._generate_display_id(parent)
  255. changes.append(f"自动完成: {parent_display_id}. {parent.description}(所有子目标已完成)")
  256. # 2. 处理 focus(切换焦点到新目标)
  257. if focus is not None:
  258. goal = tree.find_by_display_id(focus)
  259. if not goal:
  260. return f"错误:找不到目标 {focus}\n\n当前计划:\n{tree.to_prompt()}"
  261. tree.focus(goal.id)
  262. display_id = tree._generate_display_id(goal)
  263. changes.append(f"切换焦点: {display_id}. {goal.description}")
  264. # 自动注入知识
  265. inject_msg = await inject_knowledge_for_goal(
  266. goal, tree, store, trace_id, knowledge_config, sequence=context.get("sequence")
  267. )
  268. if inject_msg:
  269. changes.append(inject_msg)
  270. # 3. 处理 abandon(放弃当前目标)
  271. if abandon is not None:
  272. if not tree.current_id:
  273. return f"错误:没有当前目标可以放弃。当前焦点为空。\n\n当前计划:\n{tree.to_prompt()}"
  274. goal = tree.abandon(
  275. tree.current_id,
  276. abandon,
  277. auto_advance=not recursive_protocol,
  278. )
  279. display_id = tree._generate_display_id(goal)
  280. changes.append(f"已放弃: {display_id}. {goal.description}")
  281. # 推送事件
  282. if store and trace_id:
  283. await store.update_goal(trace_id, goal.id, status="abandoned", summary=abandon)
  284. # 4. 处理 add
  285. if add is not None:
  286. # 检查 after 和 under 互斥
  287. if after is not None and under is not None:
  288. return "错误:after 和 under 参数不能同时指定"
  289. descriptions = [d.strip() for d in add.split(",") if d.strip()]
  290. if descriptions:
  291. # 解析 reasons(与 descriptions 一一对应)
  292. reasons = None
  293. if reason:
  294. reasons = [r.strip() for r in reason.split(",")]
  295. # 如果 reasons 数量少于 descriptions,补空字符串
  296. while len(reasons) < len(descriptions):
  297. reasons.append("")
  298. # 确定添加位置
  299. if after is not None:
  300. # 在指定 goal 后面添加(同层级)
  301. target_goal = tree.find_by_display_id(after)
  302. if not target_goal:
  303. return f"错误:找不到目标 {after}\n\n当前计划:\n{tree.to_prompt()}"
  304. new_goals = tree.add_goals_after(target_goal.id, descriptions, reasons=reasons)
  305. changes.append(f"在 {tree._generate_display_id(target_goal)} 后面添加 {len(new_goals)} 个同级目标")
  306. elif under is not None:
  307. # 为指定 goal 添加子目标
  308. parent_goal = tree.find_by_display_id(under)
  309. if not parent_goal:
  310. return f"错误:找不到目标 {under}\n\n当前计划:\n{tree.to_prompt()}"
  311. new_goals = tree.add_goals(descriptions, reasons=reasons, parent_id=parent_goal.id)
  312. changes.append(f"在 {tree._generate_display_id(parent_goal)} 下添加 {len(new_goals)} 个子目标")
  313. else:
  314. # 默认行为:添加到当前焦点下(如果有焦点),否则添加到顶层
  315. parent_id = tree.current_id
  316. new_goals = tree.add_goals(descriptions, reasons=reasons, parent_id=parent_id)
  317. if parent_id:
  318. parent_display_id = tree._generate_display_id(tree.find(parent_id))
  319. changes.append(f"在 {parent_display_id} 下添加 {len(new_goals)} 个子目标")
  320. else:
  321. changes.append(f"添加 {len(new_goals)} 个顶层目标")
  322. # 推送事件
  323. if store and trace_id:
  324. for goal in new_goals:
  325. await store.add_goal(trace_id, goal)
  326. # 将完整内存树状态(含 current_id)同步到存储,
  327. # 因为 store.add_goal / update_goal 各自从磁盘加载,不包含 focus 等内存变更
  328. if store and trace_id and changes:
  329. await store.update_goal_tree(trace_id, tree)
  330. # 返回当前状态
  331. result = []
  332. if changes:
  333. result.append("## 更新")
  334. result.extend(f"- {c}" for c in changes)
  335. result.append("")
  336. result.append("## Current Plan")
  337. result.append(tree.to_prompt())
  338. return "\n".join(result)
  339. def create_goal_tool_schema() -> dict:
  340. """创建 goal 工具的 JSON Schema"""
  341. return {
  342. "name": "goal",
  343. "description": """管理执行计划。目标工具是灵活的支持系统,帮助你组织和追踪工作进度。
  344. 使用策略(按需选择):
  345. - 全局规划:先规划所有目标,再逐个执行
  346. - 渐进规划:走一步看一步,每次只创建下一个目标
  347. - 动态调整:行动中随时 abandon 不可行的目标,创建新目标
  348. 参数:
  349. - add: 添加目标(逗号分隔多个)
  350. - reason: 创建理由(逗号分隔,与 add 一一对应)
  351. - after: 在指定目标后面添加同级目标。使用目标 ID。
  352. - under: 为指定目标添加子目标。使用目标 ID。如已有子目标,追加到最后。
  353. - done: 完成当前目标,值为 summary(记录关键结论)
  354. - abandon: 放弃当前目标,值为原因
  355. - focus: 切换焦点到指定目标。使用目标 ID。
  356. 位置控制(优先使用 after):
  357. - 不指定 after/under: 添加到当前 focus 下作为子目标(无 focus 时添加到顶层)
  358. - after="X": 在目标 X 后面添加兄弟节点(同层级)
  359. - under="X": 为目标 X 添加子目标
  360. - after 和 under 不能同时指定
  361. 执行顺序:
  362. - done → focus → abandon → add
  363. - 如果同时指定 done 和 focus,会先完成当前目标,再切换焦点到新目标
  364. 示例:
  365. - goal(add="分析代码, 实现功能, 测试") - 添加顶层目标
  366. - goal(add="设计接口, 实现代码", under="2") - 为目标2添加子目标
  367. - goal(add="编写文档", after="3") - 在目标3后面添加同级任务
  368. - goal(add="集成测试", after="2.2") - 在目标2.2后面添加同级任务
  369. - goal(done="发现用户模型在 models/user.py") - 完成当前目标
  370. - goal(done="已完成调研", focus="2") - 完成当前目标,切换到目标2
  371. - goal(abandon="方案A需要Redis,环境没有") - 放弃当前目标
  372. 注意:
  373. - 目标 ID 的格式为 "1", "2", "2.1", "2.2" 等,在计划视图中可以看到
  374. - reason 应该与 add 的目标数量一致,如果数量不一致,缺少的 reason 将为空
  375. """,
  376. "parameters": {
  377. "type": "object",
  378. "properties": {
  379. "add": {
  380. "type": "string",
  381. "description": "添加目标(逗号分隔多个)"
  382. },
  383. "reason": {
  384. "type": "string",
  385. "description": "创建理由(逗号分隔多个,与 add 一一对应)。说明为什么要做这些目标。"
  386. },
  387. "after": {
  388. "type": "string",
  389. "description": "在指定目标后面添加(同层级)。使用目标的 ID,如 \"2\" 或 \"2.1\"。"
  390. },
  391. "under": {
  392. "type": "string",
  393. "description": "为指定目标添加子目标。使用目标的 ID,如 \"2\" 或 \"2.1\"。"
  394. },
  395. "done": {
  396. "type": "string",
  397. "description": "完成当前目标,值为 summary"
  398. },
  399. "abandon": {
  400. "type": "string",
  401. "description": "放弃当前目标,值为原因"
  402. },
  403. "focus": {
  404. "type": "string",
  405. "description": "切换焦点到指定目标。使用目标的 ID,如 \"2\" 或 \"2.1\"。"
  406. }
  407. },
  408. "required": []
  409. }
  410. }