subagent.py 72 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948
  1. """Sub-Agent 的创建、调度、汇合与旧评估工具。
  2. ``agent`` 由父 Agent 调用:Legacy 只创建一层,Recursive 还会执行深度、孩子数、
  3. 权限、预算和审核门禁;本地任务进程内运行。Legacy 和 Recursive
  4. revision 1 的 ``remote_*`` 仍通过 KnowHub HTTP 路由,revision 2 失败关闭。
  5. ``evaluate`` 保留给 Legacy 和旧的 Recursive revision 1;revision 2 使用框架管理的独立 Validator。
  6. """
  7. import asyncio
  8. import json
  9. import os
  10. from datetime import datetime
  11. from typing import Any, Dict, List, Optional, Union
  12. from pydantic import ValidationError
  13. from cyber_agent.tools import tool
  14. from cyber_agent.core.agent_mode import (
  15. AgentMode,
  16. AgentPolicy,
  17. RECURSIVE_CAPABILITY_TOOLS_CONTEXT_KEY,
  18. RECURSIVE_CHILD_EXECUTION_MODE_CONTEXT_KEY,
  19. RECURSIVE_MAX_PARALLEL_CHILDREN_CONTEXT_KEY,
  20. apply_policy_to_context,
  21. assert_removed_config_absent,
  22. policy_from_context,
  23. validate_recursive_child_execution,
  24. )
  25. from cyber_agent.core.task_protocol import (
  26. TaskBrief,
  27. TaskReport,
  28. ensure_task_protocol,
  29. format_task_brief,
  30. new_task_protocol,
  31. pending_review_entry,
  32. protocol_error_report,
  33. replace_task_brief,
  34. stopped_task_report,
  35. )
  36. from cyber_agent.core.context_policy import (
  37. CONTEXT_ACCESS_KEY,
  38. ContextPolicyError,
  39. build_child_context_access,
  40. context_ref_descriptors,
  41. normalize_task_brief,
  42. persist_root_task_anchor,
  43. require_matching_root_task_anchor,
  44. task_briefs_match,
  45. )
  46. from cyber_agent.core.resource_budget import (
  47. ResourceBudgetExceeded,
  48. ResourceBudgetStateError,
  49. )
  50. from cyber_agent.core.memory import compute_memory_identity
  51. from cyber_agent.core.run_snapshot import (
  52. RunConfigSnapshotV1,
  53. persist_run_config_snapshot,
  54. )
  55. from cyber_agent.trace.models import Trace, Messages
  56. from cyber_agent.trace.trace_id import generate_sub_trace_id
  57. from cyber_agent.trace.goal_models import GoalTree
  58. from cyber_agent.trace.websocket import broadcast_sub_trace_started, broadcast_sub_trace_completed
  59. from cyber_agent.tools.builtin.knowledge import KnowledgeConfig
  60. # ===== 远端路由常量 =====
  61. REMOTE_PREFIX = "remote_"
  62. # POC 阶段使用进程内锁保护“检查配额 -> 创建 Trace”。
  63. # 多 worker 部署时需要换成跨进程的原子配额。
  64. _LOCAL_AGENT_SPAWN_LOCK = asyncio.Lock()
  65. def _knowhub_api() -> str:
  66. """运行时读取 KNOWHUB_API,避免 module-load 时 .env 尚未加载的情况"""
  67. return os.getenv("KNOWHUB_API", "http://localhost:9999").rstrip("/")
  68. def _remote_agent_timeout() -> float:
  69. return float(os.getenv("REMOTE_AGENT_TIMEOUT", "600"))
  70. # 兼容旧代码对 module-level 常量的引用(运行时值 = 首次 import 时的快照)
  71. KNOWHUB_API = _knowhub_api()
  72. REMOTE_AGENT_TIMEOUT = _remote_agent_timeout()
  73. # ===== prompts =====
  74. # ===== 评估任务 =====
  75. EVALUATE_PROMPT_TEMPLATE = """# 评估任务
  76. 请评估以下任务的执行结果是否满足要求。
  77. ## 目标描述
  78. {goal_description}
  79. ## 执行结果
  80. {result_text}
  81. ## 输出格式
  82. ## 评估结论
  83. [通过/不通过]
  84. ## 评估理由
  85. [详细说明通过或不通过原因]
  86. ## 修改建议(如果不通过)
  87. 1. [建议1]
  88. 2. [建议2]
  89. """
  90. # ===== 结果格式化 =====
  91. DELEGATE_RESULT_HEADER = "## 委托任务完成\n"
  92. DELEGATE_SAVED_KNOWLEDGE_HEADER = "**保存的知识** ({count} 条):"
  93. DELEGATE_STATS_HEADER = "**执行统计**:"
  94. EXPLORE_RESULT_HEADER = "## 探索结果\n"
  95. EXPLORE_BRANCH_TEMPLATE = "### 方案 {branch_name}: {task}"
  96. EXPLORE_STATUS_SUCCESS = "**状态**: ✓ 完成"
  97. EXPLORE_STATUS_FAILED = "**状态**: ✗ 失败"
  98. EXPLORE_STATUS_ERROR = "**状态**: ✗ 异常"
  99. EXPLORE_SUMMARY_HEADER = "## 总结"
  100. def build_evaluate_prompt(goal_description: str, result_text: str) -> str:
  101. return EVALUATE_PROMPT_TEMPLATE.format(
  102. goal_description=goal_description,
  103. result_text=result_text or "(无执行结果)",
  104. )
  105. def _make_run_config(**kwargs):
  106. """延迟导入 RunConfig 以避免循环导入"""
  107. from cyber_agent.core.runner import RunConfig
  108. return RunConfig(**kwargs)
  109. # ===== 辅助函数 =====
  110. async def _update_collaborator(
  111. store, trace_id: str,
  112. name: str, sub_trace_id: str,
  113. status: str, summary: str = "",
  114. ) -> None:
  115. """
  116. 更新 trace.context["collaborators"] 中的协作者信息。
  117. 如果同名协作者已存在则更新,否则追加。
  118. """
  119. trace = await store.get_trace(trace_id)
  120. if not trace:
  121. return
  122. collaborators = trace.context.get("collaborators", [])
  123. # 查找已有记录
  124. existing = None
  125. for c in collaborators:
  126. if c.get("trace_id") == sub_trace_id:
  127. existing = c
  128. break
  129. if existing:
  130. existing["status"] = status
  131. if summary:
  132. existing["summary"] = summary
  133. else:
  134. collaborators.append({
  135. "name": name,
  136. "type": "agent",
  137. "trace_id": sub_trace_id,
  138. "status": status,
  139. "summary": summary,
  140. })
  141. trace.context["collaborators"] = collaborators
  142. await store.update_trace(trace_id, context=trace.context)
  143. async def _update_goal_start(
  144. store, trace_id: str, goal_id: str, mode: str,
  145. sub_trace_ids: List[Dict[str, str]],
  146. *,
  147. accumulate_sub_trace_ids: bool,
  148. status: str = "in_progress",
  149. ) -> None:
  150. """标记 Goal 开始执行"""
  151. if not goal_id:
  152. return
  153. next_entries = sub_trace_ids
  154. if accumulate_sub_trace_ids:
  155. tree = await store.get_goal_tree(trace_id)
  156. goal = tree.find(goal_id) if tree else None
  157. existing_entries = goal.sub_trace_ids if goal and goal.sub_trace_ids else []
  158. merged_entries: Dict[str, Dict[str, str]] = {}
  159. for entry in [*existing_entries, *sub_trace_ids]:
  160. if isinstance(entry, str):
  161. merged_entries[entry] = {"trace_id": entry, "mission": entry}
  162. elif isinstance(entry, dict) and entry.get("trace_id"):
  163. merged_entries[entry["trace_id"]] = entry
  164. next_entries = list(merged_entries.values())
  165. await store.update_goal(
  166. trace_id, goal_id,
  167. type="agent_call",
  168. agent_call_mode=mode,
  169. status=status,
  170. sub_trace_ids=next_entries,
  171. )
  172. async def _update_goal_complete(
  173. store, trace_id: str, goal_id: str,
  174. status: str, summary: str,
  175. ) -> None:
  176. """标记 Goal 完成"""
  177. if not goal_id:
  178. return
  179. await store.update_goal(
  180. trace_id, goal_id,
  181. status=status,
  182. summary=summary,
  183. )
  184. async def _load_or_create_task_report(
  185. store,
  186. child_trace_id: str,
  187. fallback_reason: str,
  188. child_result_status: str,
  189. generated_at_sequence: int,
  190. ) -> TaskReport:
  191. """读取孩子已提交的报告,或为异常、停止生成框架报告。
  192. 子任务执行结束后由报告汇合阶段调用,将异常和停止统一转成可审核的 ``TaskReport``。
  193. """
  194. child = await store.get_trace(child_trace_id)
  195. if not child:
  196. return protocol_error_report(child_trace_id, fallback_reason)
  197. state = ensure_task_protocol(child.context)
  198. report_data = state.get("task_report")
  199. execution_stopped = (
  200. child_result_status == "stopped"
  201. or child.status == "stopped"
  202. )
  203. execution_failed = (
  204. child_result_status != "completed"
  205. or child.status != "completed"
  206. )
  207. if report_data:
  208. try:
  209. report = TaskReport.model_validate(report_data)
  210. if report.child_trace_id != child_trace_id:
  211. if report.model_dump() not in state["report_history"]:
  212. state["report_history"].append(report.model_dump())
  213. fallback_reason = (
  214. "Child persisted a TaskReport whose child_trace_id does not "
  215. "match its Trace"
  216. )
  217. elif execution_stopped:
  218. if report.outcome in {"failed", "protocol_error"}:
  219. return report
  220. if report.model_dump() not in state["report_history"]:
  221. state["report_history"].append(report.model_dump())
  222. fallback_reason = "Child Agent execution was stopped"
  223. elif not execution_failed or report.outcome in {"failed", "protocol_error"}:
  224. return report
  225. else:
  226. if report.model_dump() not in state["report_history"]:
  227. state["report_history"].append(report.model_dump())
  228. fallback_reason = (
  229. f"Child execution ended as {child_result_status} after submitting "
  230. f"a {report.outcome} TaskReport: {fallback_reason}"
  231. )
  232. except ValidationError:
  233. fallback_reason = "Child persisted an invalid TaskReport"
  234. report = (
  235. stopped_task_report(child_trace_id, fallback_reason)
  236. if execution_stopped
  237. else protocol_error_report(child_trace_id, fallback_reason)
  238. )
  239. state["task_report"] = report.model_dump()
  240. state["task_report_submitted_at_sequence"] = generated_at_sequence
  241. state["task_report_validation"] = None
  242. await store.update_trace(child_trace_id, context=child.context)
  243. return report
  244. async def _record_pending_task_reports(
  245. store,
  246. runner,
  247. parent_trace: Trace,
  248. goal_id: str | None,
  249. child_results: List[tuple[str, Dict[str, Any]]],
  250. received_at_sequence: int,
  251. ) -> List[TaskReport]:
  252. """验收一批子 Agent 结果并写入父 Trace 的待审核队列。
  253. ``_run_agents`` 汇合孩子后按输入顺序调用,必要时触发 Validator 并更新 Goal 状态。
  254. """
  255. state = ensure_task_protocol(parent_trace.context)
  256. reports: List[TaskReport] = []
  257. for child_trace_id, result in child_results:
  258. reason = result.get("error") or result.get("summary") or "Child ended without TaskReport"
  259. report = await _load_or_create_task_report(
  260. store,
  261. child_trace_id,
  262. reason,
  263. result.get("status", "unknown"),
  264. received_at_sequence,
  265. )
  266. child_trace = await store.get_trace(child_trace_id)
  267. if not child_trace:
  268. raise ValueError(f"Child Trace not found for validation: {child_trace_id}")
  269. child_state = ensure_task_protocol(child_trace.context)
  270. task_brief = child_state.get("task_brief")
  271. report_payload = report.model_dump()
  272. if report.outcome in {"satisfied", "partial"}:
  273. validation_run = await runner.validate_recursive_trace(
  274. child_trace_id,
  275. scope="task",
  276. task_brief=task_brief,
  277. task_report=report_payload,
  278. )
  279. else:
  280. is_protocol_error = report.outcome == "protocol_error"
  281. validation_run = await runner.validate_recursive_trace(
  282. child_trace_id,
  283. scope="task",
  284. task_brief=task_brief,
  285. task_report=report_payload,
  286. deterministic_failure={
  287. "outcome": "error" if is_protocol_error else "failed",
  288. "reason": report.summary,
  289. "issues": report.remaining_issues or [report.summary],
  290. "retry_from": None if is_protocol_error else "task_definition",
  291. },
  292. )
  293. validation_result = validation_run.result
  294. state["pending_reviews"][child_trace_id] = pending_review_entry(
  295. goal_id=goal_id,
  296. report=report,
  297. validation_result=validation_result.model_dump(),
  298. received_at_sequence=received_at_sequence,
  299. )
  300. reports.append(report)
  301. await store.update_trace(parent_trace.trace_id, context=parent_trace.context)
  302. if goal_id:
  303. await store.update_goal(
  304. parent_trace.trace_id,
  305. goal_id,
  306. cascade_completion=False,
  307. status="pending_review",
  308. )
  309. return reports
  310. def _project_goal_status(context: dict, goal_id: str | None, status: str) -> None:
  311. tree = context.get("goal_tree")
  312. goal = tree.find(goal_id) if tree and goal_id else None
  313. if goal:
  314. goal.status = status
  315. def _aggregate_stats(results: List[Dict[str, Any]]) -> Dict[str, Any]:
  316. """聚合多个结果的统计信息"""
  317. total_messages = 0
  318. total_tokens = 0
  319. total_cost = 0.0
  320. for result in results:
  321. if isinstance(result, dict) and "stats" in result:
  322. stats = result["stats"]
  323. total_messages += stats.get("total_messages", 0)
  324. total_tokens += stats.get("total_tokens", 0)
  325. total_cost += stats.get("total_cost", 0.0)
  326. return {
  327. "total_messages": total_messages,
  328. "total_tokens": total_tokens,
  329. "total_cost": total_cost
  330. }
  331. async def _stopped_child_result(store, runner, trace_id: str) -> Dict[str, Any]:
  332. """停止尚未启动的预创建孩子,不产生任何模型调用或消息统计。"""
  333. await store.update_trace(
  334. trace_id,
  335. status="stopped",
  336. completed_at=datetime.now(),
  337. )
  338. event = getattr(runner, "_cancel_events", {}).get(trace_id)
  339. release_trace = getattr(runner, "release_recursive_trace", None)
  340. if release_trace:
  341. release_trace(trace_id, event)
  342. return {
  343. "status": "stopped",
  344. "summary": "Agent execution stopped.",
  345. "error": "Child Agent execution was stopped",
  346. "saved_knowledge_ids": [],
  347. "stats": {"total_messages": 0, "total_tokens": 0, "total_cost": 0.0},
  348. }
  349. async def _execute_child_spec(
  350. spec: Dict[str, Any],
  351. runner,
  352. store,
  353. semaphore: Optional[asyncio.Semaphore] = None,
  354. ) -> Dict[str, Any]:
  355. """按调度规格延迟启动一个子 Agent。
  356. ``_run_agents`` 串行执行或在并行信号量内调用;排队期间停止则不进入 Runner。
  357. """
  358. trace_id = spec["trace_id"]
  359. async def execute() -> Dict[str, Any]:
  360. is_cancelled = getattr(runner, "is_cancel_requested", lambda _tid: False)
  361. if spec["recursive"] and is_cancelled(trace_id):
  362. return await _stopped_child_result(store, runner, trace_id)
  363. return await runner.run_result(
  364. messages=spec["messages"],
  365. config=spec["config"],
  366. on_event=spec["on_event"],
  367. )
  368. if semaphore is None:
  369. return await execute()
  370. is_cancelled = getattr(runner, "is_cancel_requested", lambda _tid: False)
  371. if spec["recursive"] and is_cancelled(trace_id):
  372. return await _stopped_child_result(store, runner, trace_id)
  373. async with semaphore:
  374. return await execute()
  375. def _get_recursive_parent_capabilities(context: dict) -> Optional[set[str]]:
  376. """从隐藏 Tool Context 读取并校验父 Agent 的冻结权限快照。
  377. ``_run_agents`` 和子级权限计算共用;快照缺失或格式不合法时 Recursive 拒绝创建孩子。
  378. """
  379. runner = context.get("runner")
  380. capabilities = context.get(RECURSIVE_CAPABILITY_TOOLS_CONTEXT_KEY)
  381. if (
  382. not runner
  383. or not hasattr(runner, "tools")
  384. or not hasattr(runner.tools, "get_tool_names")
  385. or not isinstance(capabilities, list)
  386. or any(not isinstance(name, str) for name in capabilities)
  387. ):
  388. return None
  389. return set(runner.tools.get_tool_names()) & set(capabilities)
  390. def _get_allowed_tools(
  391. context: dict,
  392. agent_depth: int,
  393. policy: AgentPolicy,
  394. ) -> Optional[List[str]]:
  395. """按父级能力、子级固定规则和当前深度计算有效工具集合。
  396. 创建子 Runner 前调用;Recursive 权限只能逐层收紧,最深层会移除 ``agent``。
  397. """
  398. runner = context.get("runner")
  399. if runner and hasattr(runner, "tools") and hasattr(runner.tools, "get_tool_names"):
  400. registered_tools = set(runner.tools.get_tool_names())
  401. if policy.mode is AgentMode.RECURSIVE:
  402. allowed_tools = _get_recursive_parent_capabilities(context)
  403. if allowed_tools is None:
  404. return None
  405. else:
  406. # Legacy 保留原行为:从全局 Registry 取工具全集。
  407. allowed_tools = registered_tools
  408. blocked_tools = {"evaluate", "bash_command"}
  409. if not policy.requires_task_protocol:
  410. blocked_tools.update({
  411. "submit_task_report",
  412. "review_task_result",
  413. "read_context_ref",
  414. })
  415. if policy.mode is AgentMode.LEGACY or agent_depth >= policy.max_depth:
  416. blocked_tools.add("agent")
  417. return sorted(allowed_tools - blocked_tools)
  418. return None
  419. async def _resolve_trace_lineage(store, trace: Trace) -> tuple[int, str]:
  420. """返回 Trace 的 (agent_depth, root_trace_id)。
  421. 新 Trace 直接读 context;旧 Trace 缺少字段时沿 parent_trace_id
  422. 回溯,不依赖 Trace ID 字符串格式。
  423. """
  424. depth = trace.context.get("agent_depth")
  425. root_trace_id = trace.context.get("root_trace_id")
  426. if isinstance(depth, int) and depth >= 0 and isinstance(root_trace_id, str):
  427. return depth, root_trace_id
  428. depth = 0
  429. current = trace
  430. root_trace_id = trace.trace_id
  431. visited = {trace.trace_id}
  432. while current.parent_trace_id:
  433. parent_id = current.parent_trace_id
  434. if parent_id in visited:
  435. break
  436. visited.add(parent_id)
  437. parent = await store.get_trace(parent_id)
  438. if not parent:
  439. break
  440. depth += 1
  441. root_trace_id = parent.trace_id
  442. current = parent
  443. return depth, root_trace_id
  444. async def _load_root_task_anchor(store, trace: Trace):
  445. """从权威根 Trace读取 Anchor,并校验当前节点持有相同副本。"""
  446. root_trace_id = trace.context.get("root_trace_id") or trace.trace_id
  447. root = trace if root_trace_id == trace.trace_id else await store.get_trace(root_trace_id)
  448. if not root:
  449. raise ContextPolicyError(f"Recursive root Trace not found: {root_trace_id}")
  450. anchor = require_matching_root_task_anchor(root.context, trace.context)
  451. return root, anchor
  452. def _format_single_result(result: Dict[str, Any], sub_trace_id: str, continued: bool) -> Dict[str, Any]:
  453. """格式化单任务(delegate)结果"""
  454. lines = [DELEGATE_RESULT_HEADER]
  455. summary = result.get("summary", "")
  456. if summary:
  457. lines.append(summary)
  458. lines.append("")
  459. # 添加保存的知识 ID
  460. saved_knowledge_ids = result.get("saved_knowledge_ids", [])
  461. if saved_knowledge_ids:
  462. lines.append("---\n")
  463. lines.append(DELEGATE_SAVED_KNOWLEDGE_HEADER.format(count=len(saved_knowledge_ids)))
  464. for kid in saved_knowledge_ids:
  465. lines.append(f"- {kid}")
  466. lines.append("")
  467. lines.append("---\n")
  468. lines.append(DELEGATE_STATS_HEADER)
  469. stats = result.get("stats", {})
  470. if stats:
  471. lines.append(f"- 消息数: {stats.get('total_messages', 0)}")
  472. lines.append(f"- Tokens: {stats.get('total_tokens', 0)}")
  473. lines.append(f"- 成本: ${stats.get('total_cost', 0.0):.4f}")
  474. formatted_summary = "\n".join(lines)
  475. return {
  476. "mode": "delegate",
  477. "sub_trace_id": sub_trace_id,
  478. "continue_from": continued,
  479. "saved_knowledge_ids": saved_knowledge_ids, # 传递给父 agent
  480. **result,
  481. "summary": formatted_summary,
  482. }
  483. def _format_multi_result(
  484. tasks: List[str], results: List[Dict[str, Any]], sub_trace_ids: List[Dict]
  485. ) -> Dict[str, Any]:
  486. """格式化多任务(explore)聚合结果"""
  487. lines = [EXPLORE_RESULT_HEADER]
  488. successful = 0
  489. failed = 0
  490. total_tokens = 0
  491. total_cost = 0.0
  492. for i, (task_item, result) in enumerate(zip(tasks, results)):
  493. branch_name = chr(ord('A') + i)
  494. lines.append(EXPLORE_BRANCH_TEMPLATE.format(branch_name=branch_name, task=task_item))
  495. if isinstance(result, dict):
  496. status = result.get("status", "unknown")
  497. if status == "completed":
  498. lines.append(EXPLORE_STATUS_SUCCESS)
  499. successful += 1
  500. else:
  501. lines.append(EXPLORE_STATUS_FAILED)
  502. failed += 1
  503. summary = result.get("summary", "")
  504. if summary:
  505. lines.append(f"**摘要**: {summary[:200]}...")
  506. stats = result.get("stats", {})
  507. if stats:
  508. messages = stats.get("total_messages", 0)
  509. tokens = stats.get("total_tokens", 0)
  510. cost = stats.get("total_cost", 0.0)
  511. lines.append(f"**统计**: {messages} messages, {tokens} tokens, ${cost:.4f}")
  512. total_tokens += tokens
  513. total_cost += cost
  514. else:
  515. lines.append(EXPLORE_STATUS_ERROR)
  516. failed += 1
  517. lines.append("")
  518. lines.append("---\n")
  519. lines.append(EXPLORE_SUMMARY_HEADER)
  520. lines.append(f"- 总分支数: {len(tasks)}")
  521. lines.append(f"- 成功: {successful}")
  522. lines.append(f"- 失败: {failed}")
  523. lines.append(f"- 总 tokens: {total_tokens}")
  524. lines.append(f"- 总成本: ${total_cost:.4f}")
  525. aggregated_summary = "\n".join(lines)
  526. overall_status = "completed" if successful > 0 else "failed"
  527. return {
  528. "mode": "explore",
  529. "status": overall_status,
  530. "summary": aggregated_summary,
  531. "sub_trace_ids": sub_trace_ids,
  532. "tasks": tasks,
  533. "stats": _aggregate_stats(results),
  534. }
  535. async def _get_goal_description(store, trace_id: str, goal_id: str) -> str:
  536. """从 GoalTree 获取目标描述"""
  537. if not goal_id:
  538. return ""
  539. goal_tree = await store.get_goal_tree(trace_id)
  540. if goal_tree:
  541. target_goal = goal_tree.find(goal_id)
  542. if target_goal:
  543. return target_goal.description
  544. return f"Goal {goal_id}"
  545. def _build_evaluate_prompt(goal_description: str, messages: Optional[Messages]) -> str:
  546. """
  547. 构建评估 prompt。
  548. Args:
  549. goal_description: 代码从 GoalTree 注入的目标描述
  550. messages: 模型提供的消息(执行结果+上下文)
  551. """
  552. # 从 messages 提取文本内容
  553. result_text = ""
  554. if messages:
  555. parts = []
  556. for msg in messages:
  557. content = msg.get("content", "")
  558. if isinstance(content, str):
  559. parts.append(content)
  560. elif isinstance(content, list):
  561. # 多模态内容,提取文本部分
  562. for item in content:
  563. if isinstance(item, dict) and item.get("type") == "text":
  564. parts.append(item.get("text", ""))
  565. result_text = "\n".join(parts)
  566. return build_evaluate_prompt(goal_description, result_text)
  567. def _make_event_printer(label: str):
  568. """
  569. 创建子 Agent 执行过程打印函数。
  570. 当父 runner.debug=True 时,传给 run_result(on_event=...),
  571. 实时输出子 Agent 的工具调用和助手消息。
  572. """
  573. prefix = f" [{label}]"
  574. def on_event(item):
  575. from cyber_agent.trace.models import Trace, Message
  576. if isinstance(item, Message):
  577. if item.role == "assistant":
  578. content = item.content
  579. if isinstance(content, dict):
  580. text = content.get("text", "")
  581. tool_calls = content.get("tool_calls")
  582. if text:
  583. preview = text[:120] + "..." if len(text) > 120 else text
  584. print(f"{prefix} {preview}")
  585. if tool_calls:
  586. for tc in tool_calls:
  587. name = tc.get("function", {}).get("name", "unknown")
  588. print(f"{prefix} 🛠️ {name}")
  589. elif item.role == "tool":
  590. content = item.content
  591. if isinstance(content, dict):
  592. name = content.get("tool_name", "unknown")
  593. desc = item.description or ""
  594. desc_short = (desc[:60] + "...") if len(desc) > 60 else desc
  595. suffix = f": {desc_short}" if desc_short else ""
  596. print(f"{prefix} ✅ {name}{suffix}")
  597. elif isinstance(item, Trace):
  598. if item.status == "completed":
  599. print(f"{prefix} ✓ 完成")
  600. elif item.status == "failed":
  601. err = (item.error_message or "")[:80]
  602. print(f"{prefix} ✗ 失败: {err}")
  603. return on_event
  604. def _make_interactive_handler(runner, sub_trace_id: str, parent_trace_id: str, debug_printer=None):
  605. """
  606. 创建支持 stdin 交互检查的 on_event 回调。
  607. 在每个子 Agent 事件触发时检查 stdin,检测到暂停/退出信号后
  608. 通过 cancel_event.set() 停止子 agent 和父 agent 的执行。
  609. """
  610. def on_event(item):
  611. # 先执行 debug 打印
  612. if debug_printer:
  613. debug_printer(item)
  614. # 检查 stdin
  615. check_fn = getattr(runner, 'stdin_check', None)
  616. if not check_fn:
  617. return
  618. cmd = check_fn()
  619. if cmd in ('pause', 'quit'):
  620. request_stop = getattr(runner, "request_stop", None)
  621. if request_stop:
  622. request_stop(parent_trace_id)
  623. else:
  624. for tid in (sub_trace_id, parent_trace_id):
  625. ev = runner._cancel_events.get(tid)
  626. if ev:
  627. ev.set()
  628. return on_event
  629. # ===== 统一内部执行函数 =====
  630. async def _run_agents(
  631. tasks: List[str],
  632. per_agent_msgs: List[Messages],
  633. continue_from: Optional[str],
  634. store, trace_id: str, goal_id: str, runner, context: dict,
  635. agent_type: Optional[str] = None,
  636. skills: Optional[List[str]] = None,
  637. task_briefs: Optional[List[TaskBrief]] = None,
  638. ) -> Dict[str, Any]:
  639. """
  640. 本地 Sub-Agent 的统一创建、调度和结果汇合入口。
  641. ``agent`` 完成参数归一化后调用;此处执行 Spawn Guard、权限交集、串并行调度和
  642. 报告汇合,并始终使用父 Trace 已持久化的 ``AgentPolicy``。
  643. """
  644. single = len(tasks) == 1
  645. parent_trace = await store.get_trace(trace_id)
  646. if not parent_trace:
  647. return {"status": "failed", "error": f"Parent trace not found: {trace_id}"}
  648. try:
  649. policy = policy_from_context(parent_trace.context)
  650. except ValueError as exc:
  651. return {"status": "failed", "error": str(exc)}
  652. root_task_anchor = None
  653. if policy.requires_task_protocol:
  654. try:
  655. _, root_task_anchor = await _load_root_task_anchor(
  656. store,
  657. parent_trace,
  658. )
  659. except ContextPolicyError as exc:
  660. return {"status": "failed", "error": str(exc)}
  661. if (
  662. policy.mode is AgentMode.RECURSIVE
  663. and _get_recursive_parent_capabilities(context) is None
  664. ):
  665. return {
  666. "status": "failed",
  667. "error": "Recursive agent capability context is missing or invalid",
  668. }
  669. child_execution_mode = "parallel"
  670. max_parallel_children = 2
  671. if policy.mode is AgentMode.RECURSIVE:
  672. try:
  673. child_execution_mode, max_parallel_children = (
  674. validate_recursive_child_execution(
  675. context.get(
  676. RECURSIVE_CHILD_EXECUTION_MODE_CONTEXT_KEY,
  677. "sequential",
  678. ),
  679. context.get(
  680. RECURSIVE_MAX_PARALLEL_CHILDREN_CONTEXT_KEY,
  681. 2,
  682. ),
  683. )
  684. )
  685. except ValueError as exc:
  686. return {"status": "failed", "error": str(exc)}
  687. protocol_state = None
  688. approved_action = None
  689. if policy.requires_task_protocol:
  690. protocol_state = ensure_task_protocol(parent_trace.context)
  691. parent_task_brief = protocol_state.get("task_brief")
  692. if goal_id:
  693. parent_tree = await store.get_goal_tree(trace_id)
  694. parent_goal = parent_tree.find(goal_id) if parent_tree else None
  695. if parent_goal and parent_goal.status in {"completed", "failed", "abandoned"}:
  696. return {
  697. "status": "failed",
  698. "error": "Cannot create a child from a terminal Goal",
  699. }
  700. if protocol_state["pending_reviews"]:
  701. return {
  702. "status": "failed",
  703. "error": "Review pending child TaskReports before creating another child",
  704. }
  705. if not task_briefs or len(task_briefs) != len(tasks):
  706. return {"status": "failed", "error": "Recursive revision 2 requires task_brief"}
  707. if protocol_state["next_actions"]:
  708. approved_action = protocol_state["next_actions"][0]
  709. decision = approved_action["decision"]
  710. expected_brief = approved_action.get("task_brief")
  711. if decision == "REVISE_CHILD":
  712. if continue_from != approved_action["child_trace_id"] or len(tasks) != 1:
  713. return {
  714. "status": "failed",
  715. "error": "REVISE_CHILD requires continue_from for the reviewed child",
  716. }
  717. try:
  718. matches_approved = not expected_brief or task_briefs_match(
  719. task_briefs[0],
  720. expected_brief,
  721. parent_task_brief=parent_task_brief,
  722. root_task_anchor=root_task_anchor,
  723. )
  724. except ContextPolicyError as exc:
  725. return {
  726. "status": "failed",
  727. "error": f"Approved Recursive TaskBrief is invalid; recreate the trace: {exc}",
  728. }
  729. if not matches_approved:
  730. return {
  731. "status": "failed",
  732. "error": "task_brief does not match the approved revision",
  733. }
  734. else:
  735. if continue_from or len(tasks) != 1:
  736. return {
  737. "status": "failed",
  738. "error": f"{decision} requires exactly one new approved child",
  739. }
  740. try:
  741. matches_approved = bool(expected_brief) and task_briefs_match(
  742. task_briefs[0],
  743. expected_brief,
  744. parent_task_brief=parent_task_brief,
  745. root_task_anchor=root_task_anchor,
  746. )
  747. except ContextPolicyError as exc:
  748. return {
  749. "status": "failed",
  750. "error": f"Approved Recursive TaskBrief is invalid; recreate the trace: {exc}",
  751. }
  752. if not matches_approved:
  753. return {
  754. "status": "failed",
  755. "error": "task_brief does not match the approved next task",
  756. }
  757. elif continue_from:
  758. return {
  759. "status": "failed",
  760. "error": "continue_from requires a pending REVISE_CHILD action",
  761. }
  762. # continue_from: 复用已有 trace(仅 single)
  763. sub_trace_id = None
  764. continued = False
  765. goal_started = False
  766. child_records: List[Dict[str, Any]] = []
  767. all_sub_trace_ids: List[Dict[str, str]] = []
  768. created_trace_ids: list[str] = []
  769. async def fail_created_children(error: Exception) -> None:
  770. """将已创建但未进入执行阶段的 Recursive 孩子收敛为失败。"""
  771. for created_trace_id in created_trace_ids:
  772. created = await store.get_trace(created_trace_id)
  773. if created and created.status == "running":
  774. await store.update_trace(
  775. created_trace_id,
  776. status="failed",
  777. error_message=f"Child initialization failed: {error}",
  778. completed_at=datetime.now(),
  779. )
  780. async def run_initialization(operation):
  781. """执行一次运行前持久化,失败时统一清理已创建孩子。"""
  782. try:
  783. return await operation
  784. except Exception as exc:
  785. if policy.requires_task_protocol:
  786. await fail_created_children(exc)
  787. raise
  788. if single and continue_from:
  789. existing = await store.get_trace(continue_from)
  790. if not existing:
  791. return {"status": "failed", "error": f"Continue-from trace not found: {continue_from}"}
  792. if existing.parent_trace_id != trace_id:
  793. return {
  794. "status": "failed",
  795. "error": "continue_from must reference a direct child of the current trace",
  796. }
  797. if existing.uid != parent_trace.uid:
  798. return {
  799. "status": "failed",
  800. "error": "continue_from trace owner does not match the current trace owner",
  801. }
  802. if existing.context.get("created_by_tool") != "agent":
  803. return {
  804. "status": "failed",
  805. "error": "continue_from must reference a child created by the agent tool",
  806. }
  807. try:
  808. child_policy = policy_from_context(existing.context)
  809. except ValueError as exc:
  810. return {"status": "failed", "error": str(exc)}
  811. if (
  812. child_policy.mode is not policy.mode
  813. or child_policy.revision != policy.revision
  814. ):
  815. return {
  816. "status": "failed",
  817. "error": "continue_from trace Agent mode does not match the current trace",
  818. }
  819. sub_trace_id = continue_from
  820. continued = True
  821. goal_tree = await store.get_goal_tree(continue_from)
  822. mission = goal_tree.mission if goal_tree else tasks[0]
  823. parent_depth, expected_root_trace_id = await _resolve_trace_lineage(
  824. store, parent_trace
  825. )
  826. child_depth, root_trace_id = await _resolve_trace_lineage(store, existing)
  827. if (
  828. child_depth != parent_depth + 1
  829. or root_trace_id != expected_root_trace_id
  830. ):
  831. return {
  832. "status": "failed",
  833. "error": "continue_from trace lineage does not match the current trace",
  834. }
  835. if child_depth > policy.max_depth:
  836. return {
  837. "status": "failed",
  838. "error": (
  839. "continue_from trace exceeds the persisted Agent mode depth: "
  840. f"depth={child_depth}, max={policy.max_depth}"
  841. ),
  842. }
  843. all_sub_trace_ids = [{"trace_id": sub_trace_id, "mission": mission}]
  844. child_context = apply_policy_to_context({
  845. **existing.context,
  846. "agent_depth": child_depth,
  847. "root_trace_id": root_trace_id,
  848. }, policy)
  849. child_records = [{
  850. "trace_id": sub_trace_id,
  851. "depth": child_depth,
  852. "context": child_context,
  853. "is_new": False,
  854. }]
  855. if task_briefs:
  856. assert root_task_anchor is not None
  857. child_state = ensure_task_protocol(child_context)
  858. normalized_dump = task_briefs[0].model_dump(mode="json")
  859. new_context_access = build_child_context_access(
  860. parent_context=parent_trace.context,
  861. parent_trace_id=parent_trace.trace_id,
  862. root_trace_id=root_trace_id,
  863. uid=parent_trace.uid,
  864. parent_task_state=protocol_state,
  865. child_task_brief=task_briefs[0],
  866. root_task_anchor=root_task_anchor,
  867. granted_at_sequence=(existing.last_sequence or 0) + 1,
  868. )
  869. if child_state.get("task_brief") != normalized_dump:
  870. replace_task_brief(
  871. child_state,
  872. task_briefs[0],
  873. effective_at_sequence=(existing.last_sequence or 0) + 1,
  874. )
  875. child_context[CONTEXT_ACCESS_KEY] = new_context_access
  876. persist_root_task_anchor(child_context, root_task_anchor)
  877. existing.context = child_context
  878. await store.update_trace(existing.trace_id, context=existing.context)
  879. else:
  880. parent_depth, root_trace_id = await _resolve_trace_lineage(store, parent_trace)
  881. if parent_depth >= policy.max_depth:
  882. return {
  883. "status": "failed",
  884. "error": (
  885. f"Local Sub-Agent depth limit reached: "
  886. f"depth={parent_depth}, max={policy.max_depth}, "
  887. f"mode={policy.mode.value}"
  888. ),
  889. }
  890. prepared_context_accesses: list[dict[str, Any]] = []
  891. if policy.requires_task_protocol:
  892. assert root_task_anchor is not None and task_briefs is not None
  893. try:
  894. prepared_context_accesses = [
  895. build_child_context_access(
  896. parent_context=parent_trace.context,
  897. parent_trace_id=parent_trace.trace_id,
  898. root_trace_id=root_trace_id,
  899. uid=parent_trace.uid,
  900. parent_task_state=protocol_state,
  901. child_task_brief=brief,
  902. root_task_anchor=root_task_anchor,
  903. )
  904. for brief in task_briefs
  905. ]
  906. except ContextPolicyError as exc:
  907. return {
  908. "status": "failed",
  909. "error": f"Invalid TaskBrief context references: {exc}",
  910. }
  911. async def create_child_traces() -> None:
  912. child_depth = parent_depth + 1
  913. for i, task_item in enumerate(tasks):
  914. task_label = (
  915. task_briefs[i].objective
  916. if task_briefs
  917. else task_item
  918. )
  919. resolved_agent_type = agent_type or ("delegate" if single else "explore")
  920. suffix = "delegate" if single else f"explore-{i+1:03d}"
  921. stid = generate_sub_trace_id(trace_id, suffix)
  922. child_context = apply_policy_to_context({
  923. "created_by_tool": "agent",
  924. "agent_depth": child_depth,
  925. "root_trace_id": root_trace_id,
  926. }, policy)
  927. if policy.requires_task_protocol:
  928. assert root_task_anchor is not None
  929. child_context["task_protocol"] = new_task_protocol(
  930. task_briefs[i]
  931. )
  932. child_context[CONTEXT_ACCESS_KEY] = prepared_context_accesses[i]
  933. persist_root_task_anchor(child_context, root_task_anchor)
  934. sub_trace = Trace(
  935. trace_id=stid,
  936. mode="agent",
  937. task=task_label,
  938. parent_trace_id=trace_id,
  939. parent_goal_id=goal_id,
  940. agent_type=resolved_agent_type,
  941. uid=parent_trace.uid,
  942. model=parent_trace.model,
  943. status="running",
  944. context=child_context,
  945. created_at=datetime.now(),
  946. )
  947. await store.create_trace(sub_trace)
  948. # A successfully persisted Trace has consumed its Agent budget,
  949. # even if a later GoalTree write fails.
  950. created_trace_ids.append(stid)
  951. await store.update_goal_tree(stid, GoalTree(mission=task_label))
  952. all_sub_trace_ids.append({"trace_id": stid, "mission": task_label})
  953. child_records.append({
  954. "trace_id": stid,
  955. "depth": child_depth,
  956. "context": child_context,
  957. "is_new": True,
  958. "agent_type": resolved_agent_type,
  959. })
  960. if single:
  961. nonlocal sub_trace_id
  962. sub_trace_id = child_records[0]["trace_id"]
  963. child_limit = policy.max_children_per_parent
  964. if child_limit is None:
  965. await create_child_traces()
  966. else:
  967. # Recursive 模式在单进程临界区内完成“计数 -> 创建”,
  968. # 保证并发批次不会共同突破六个直接孩子。
  969. async with _LOCAL_AGENT_SPAWN_LOCK:
  970. existing_children = await store.list_traces(
  971. parent_trace_id=trace_id,
  972. created_by_tool="agent",
  973. limit=child_limit + 1,
  974. )
  975. requested_children = len(tasks)
  976. if len(existing_children) + requested_children > child_limit:
  977. return {
  978. "status": "failed",
  979. "error": (
  980. "Local child Agent limit exceeded: "
  981. f"existing={len(existing_children)}, "
  982. f"requested={requested_children}, max={child_limit}"
  983. ),
  984. }
  985. resolved_budget = None
  986. if policy.requires_task_protocol:
  987. resolved_budget = await runner._resource_budget_for_trace(trace_id)
  988. reserved_agents = 0
  989. if resolved_budget is not None:
  990. root_trace_id, budget = resolved_budget
  991. if not runner.resource_budget:
  992. return {
  993. "status": "failed",
  994. "error": "ResourceBudgetController is unavailable",
  995. }
  996. try:
  997. await runner.resource_budget.reserve_agents(
  998. root_trace_id,
  999. budget,
  1000. requested_children,
  1001. )
  1002. reserved_agents = requested_children
  1003. except (ResourceBudgetExceeded, ResourceBudgetStateError) as exc:
  1004. return {"status": "failed", "error": str(exc)}
  1005. try:
  1006. await create_child_traces()
  1007. except Exception as exc:
  1008. created_count = len(created_trace_ids)
  1009. uncreated = max(0, reserved_agents - created_count)
  1010. if uncreated:
  1011. await runner.resource_budget.release_agents(
  1012. root_trace_id,
  1013. budget,
  1014. uncreated,
  1015. )
  1016. await fail_created_children(exc)
  1017. raise
  1018. await run_initialization(
  1019. _update_goal_start(
  1020. store, trace_id, goal_id,
  1021. "delegate" if single else "explore",
  1022. all_sub_trace_ids,
  1023. accumulate_sub_trace_ids=policy.accumulate_sub_trace_ids,
  1024. status=("waiting_children" if policy.requires_task_protocol else "in_progress"),
  1025. )
  1026. )
  1027. if policy.requires_task_protocol:
  1028. _project_goal_status(context, goal_id, "waiting_children")
  1029. goal_started = True
  1030. if approved_action and protocol_state is not None:
  1031. protocol_state["next_actions"].pop(0)
  1032. protocol_state["protocol_correction_attempts"] = 0
  1033. await run_initialization(
  1034. store.update_trace(trace_id, context=parent_trace.context)
  1035. )
  1036. # 创建延迟执行规格。Trace 已按批次预创建,但不会提前创建 coroutine,
  1037. # 因而排队孩子被停止时不会产生未 await coroutine 或模型调用。
  1038. execution_specs = []
  1039. for i, (task_item, msgs, child_record) in enumerate(
  1040. zip(tasks, per_agent_msgs, child_records)
  1041. ):
  1042. cur_stid = child_record["trace_id"]
  1043. child_depth = child_record["depth"]
  1044. task_label = task_briefs[i].objective if task_briefs else task_item
  1045. if child_record["is_new"]:
  1046. await run_initialization(
  1047. broadcast_sub_trace_started(
  1048. trace_id, cur_stid, goal_id or "",
  1049. child_record["agent_type"], task_label,
  1050. )
  1051. )
  1052. # 注册为活跃协作者
  1053. collab_name = task_label[:30] if single and not continued else (
  1054. f"delegate-{cur_stid[:8]}" if single else f"explore-{i+1}"
  1055. )
  1056. await run_initialization(
  1057. _update_collaborator(
  1058. store, trace_id,
  1059. name=collab_name, sub_trace_id=cur_stid,
  1060. status="running", summary=task_label[:80],
  1061. )
  1062. )
  1063. # 构建消息
  1064. if policy.requires_task_protocol:
  1065. assert task_briefs is not None and root_task_anchor is not None
  1066. task_item = format_task_brief(
  1067. task_briefs[i],
  1068. available_context_refs=context_ref_descriptors(
  1069. child_record["context"]
  1070. ),
  1071. )
  1072. agent_msgs = list(msgs) + [{"role": "user", "content": task_item}]
  1073. allowed_tools = _get_allowed_tools(context, child_depth, policy)
  1074. debug = getattr(runner, 'debug', False)
  1075. agent_label = (agent_type or ("delegate" if single else f"explore-{i+1}"))
  1076. debug_printer = _make_event_printer(agent_label) if debug else None
  1077. # allowed_tools 已是当前深度的精确白名单。
  1078. on_event = _make_interactive_handler(
  1079. runner, cur_stid, trace_id, debug_printer=debug_printer
  1080. )
  1081. child_config = _make_run_config(
  1082. trace_id=cur_stid,
  1083. agent_type=agent_type or ("delegate" if single else "explore"),
  1084. max_iterations=50,
  1085. model=parent_trace.model if parent_trace else "gpt-4o",
  1086. uid=parent_trace.uid if parent_trace else None,
  1087. tools=allowed_tools,
  1088. tool_groups=[], # tools 是精确白名单,不再合并默认 core 组
  1089. name=task_label[:50],
  1090. skills=skills,
  1091. knowledge=context.get("knowledge_config") or KnowledgeConfig(),
  1092. context=child_record["context"],
  1093. child_execution_mode=child_execution_mode,
  1094. max_parallel_children=max_parallel_children,
  1095. )
  1096. # Recursive children are persisted before their coroutine is scheduled so
  1097. # the parent can account for queued work. Bind the immutable run snapshot
  1098. # before execution; otherwise the first local resume would look exactly
  1099. # like an unsafe pre-snapshot historical Recursive trace.
  1100. if child_record["is_new"]:
  1101. persisted_child = await store.get_trace(cur_stid)
  1102. if not persisted_child:
  1103. raise RuntimeError(f"pre-created child Trace disappeared: {cur_stid}")
  1104. child_memory_identity = (
  1105. compute_memory_identity(child_config.memory)
  1106. if child_config.memory
  1107. else None
  1108. )
  1109. child_snapshot = RunConfigSnapshotV1.from_run_config(
  1110. child_config,
  1111. memory_identity=child_memory_identity,
  1112. )
  1113. persist_run_config_snapshot(persisted_child.context, child_snapshot)
  1114. await store.update_trace(
  1115. cur_stid,
  1116. context=persisted_child.context,
  1117. )
  1118. execution_specs.append({
  1119. "index": i,
  1120. "trace_id": cur_stid,
  1121. "collaborator": collab_name,
  1122. "messages": agent_msgs,
  1123. "config": child_config,
  1124. "on_event": on_event,
  1125. "recursive": policy.mode is AgentMode.RECURSIVE,
  1126. })
  1127. # continue_from 不进入新建临界区,但仍需要恢复 Goal 的运行状态。
  1128. if not goal_started:
  1129. await run_initialization(
  1130. _update_goal_start(
  1131. store, trace_id, goal_id,
  1132. "delegate" if single else "explore",
  1133. all_sub_trace_ids,
  1134. accumulate_sub_trace_ids=policy.accumulate_sub_trace_ids,
  1135. status=("waiting_children" if policy.requires_task_protocol else "in_progress"),
  1136. )
  1137. )
  1138. if policy.requires_task_protocol:
  1139. _project_goal_status(context, goal_id, "waiting_children")
  1140. if policy.mode is AgentMode.RECURSIVE:
  1141. register_child = getattr(runner, "register_recursive_child", None)
  1142. if register_child:
  1143. for spec in execution_specs:
  1144. register_child(trace_id, spec["trace_id"])
  1145. # 执行
  1146. if single:
  1147. # 单任务直接执行(带异常处理)
  1148. spec = execution_specs[0]
  1149. stid = spec["trace_id"]
  1150. collab_name = spec["collaborator"]
  1151. try:
  1152. result = await _execute_child_spec(spec, runner, store)
  1153. await broadcast_sub_trace_completed(
  1154. trace_id, stid,
  1155. result.get("status", "completed"),
  1156. result.get("summary", ""),
  1157. result.get("stats", {}),
  1158. )
  1159. await _update_collaborator(
  1160. store, trace_id,
  1161. name=collab_name, sub_trace_id=stid,
  1162. status=result.get("status", "completed"),
  1163. summary=result.get("summary", "")[:80],
  1164. )
  1165. formatted = _format_single_result(result, stid, continued)
  1166. if policy.requires_task_protocol:
  1167. reports = await _record_pending_task_reports(
  1168. store,
  1169. runner,
  1170. parent_trace,
  1171. goal_id,
  1172. [(stid, result)],
  1173. context.get("sequence", 0),
  1174. )
  1175. formatted["task_report"] = reports[0].model_dump()
  1176. formatted["validation_result"] = ensure_task_protocol(
  1177. parent_trace.context
  1178. )["pending_reviews"][stid]["validation_result"]
  1179. _project_goal_status(context, goal_id, "pending_review")
  1180. else:
  1181. await _update_goal_complete(
  1182. store, trace_id, goal_id,
  1183. result.get("status", "completed"),
  1184. formatted["summary"],
  1185. )
  1186. return formatted
  1187. except Exception as e:
  1188. error_msg = str(e)
  1189. await broadcast_sub_trace_completed(
  1190. trace_id, stid, "failed", error_msg, {},
  1191. )
  1192. await _update_collaborator(
  1193. store, trace_id,
  1194. name=collab_name, sub_trace_id=stid,
  1195. status="failed", summary=error_msg[:80],
  1196. )
  1197. failed_result = {
  1198. "mode": "delegate",
  1199. "status": "failed",
  1200. "error": error_msg,
  1201. "sub_trace_id": stid,
  1202. }
  1203. if policy.requires_task_protocol:
  1204. reports = await _record_pending_task_reports(
  1205. store,
  1206. runner,
  1207. parent_trace,
  1208. goal_id,
  1209. [(stid, failed_result)],
  1210. context.get("sequence", 0),
  1211. )
  1212. failed_result["task_report"] = reports[0].model_dump()
  1213. failed_result["validation_result"] = ensure_task_protocol(
  1214. parent_trace.context
  1215. )["pending_reviews"][stid]["validation_result"]
  1216. _project_goal_status(context, goal_id, "pending_review")
  1217. else:
  1218. await _update_goal_complete(
  1219. store, trace_id, goal_id,
  1220. "failed", f"委托任务失败: {error_msg}",
  1221. )
  1222. return failed_result
  1223. else:
  1224. async def execute_captured(
  1225. spec: Dict[str, Any],
  1226. semaphore: Optional[asyncio.Semaphore] = None,
  1227. ):
  1228. try:
  1229. return await _execute_child_spec(spec, runner, store, semaphore)
  1230. except Exception as exc:
  1231. return exc
  1232. if policy.mode is AgentMode.LEGACY:
  1233. # Legacy 冻结旧行为:多任务整批并行,不读取新配置。
  1234. raw_results = await asyncio.gather(*(
  1235. execute_captured(spec) for spec in execution_specs
  1236. ))
  1237. elif child_execution_mode == "parallel":
  1238. semaphore = asyncio.Semaphore(max_parallel_children)
  1239. raw_results = await asyncio.gather(*(
  1240. execute_captured(spec, semaphore) for spec in execution_specs
  1241. ))
  1242. else:
  1243. raw_results = []
  1244. for spec in execution_specs:
  1245. raw_results.append(await execute_captured(spec))
  1246. processed_results = []
  1247. for idx, raw in enumerate(raw_results):
  1248. spec = execution_specs[idx]
  1249. stid = spec["trace_id"]
  1250. collab_name = spec["collaborator"]
  1251. if isinstance(raw, Exception):
  1252. error_result = {
  1253. "status": "failed",
  1254. "summary": f"执行出错: {str(raw)}",
  1255. "stats": {"total_messages": 0, "total_tokens": 0, "total_cost": 0.0},
  1256. }
  1257. processed_results.append(error_result)
  1258. await broadcast_sub_trace_completed(
  1259. trace_id, stid, "failed", str(raw), {},
  1260. )
  1261. await _update_collaborator(
  1262. store, trace_id,
  1263. name=collab_name, sub_trace_id=stid,
  1264. status="failed", summary=str(raw)[:80],
  1265. )
  1266. else:
  1267. processed_results.append(raw)
  1268. await broadcast_sub_trace_completed(
  1269. trace_id, stid,
  1270. raw.get("status", "completed"),
  1271. raw.get("summary", ""),
  1272. raw.get("stats", {}),
  1273. )
  1274. await _update_collaborator(
  1275. store, trace_id,
  1276. name=collab_name, sub_trace_id=stid,
  1277. status=raw.get("status", "completed"),
  1278. summary=raw.get("summary", "")[:80],
  1279. )
  1280. formatted = _format_multi_result(tasks, processed_results, all_sub_trace_ids)
  1281. if policy.requires_task_protocol:
  1282. reports = await _record_pending_task_reports(
  1283. store,
  1284. runner,
  1285. parent_trace,
  1286. goal_id,
  1287. [
  1288. (entry["trace_id"], processed_results[index])
  1289. for index, entry in enumerate(all_sub_trace_ids)
  1290. ],
  1291. context.get("sequence", 0),
  1292. )
  1293. formatted["task_reports"] = [report.model_dump() for report in reports]
  1294. pending = ensure_task_protocol(parent_trace.context)["pending_reviews"]
  1295. formatted["validation_results"] = [
  1296. pending[entry["trace_id"]]["validation_result"]
  1297. for entry in all_sub_trace_ids
  1298. ]
  1299. _project_goal_status(context, goal_id, "pending_review")
  1300. else:
  1301. await _update_goal_complete(
  1302. store, trace_id, goal_id,
  1303. formatted["status"],
  1304. formatted["summary"],
  1305. )
  1306. return formatted
  1307. # ===== 远端 Agent 路由 =====
  1308. async def _run_remote_agent(
  1309. agent_type: str,
  1310. task: str,
  1311. messages: Optional[Messages],
  1312. continue_from: Optional[str],
  1313. skills: Optional[List[str]] = None,
  1314. ) -> Dict[str, Any]:
  1315. """
  1316. 通过 HTTP 调用 KnowHub 服务器上的远端 Agent。
  1317. 远端 Agent 的 tools / model / prompt 由服务器端 preset 决定。
  1318. skills 由 caller 指定并原样发给服务器,最终权限由远端实现决定。
  1319. """
  1320. import httpx
  1321. payload = {
  1322. "agent_type": agent_type,
  1323. "task": task,
  1324. "messages": messages,
  1325. "continue_from": continue_from,
  1326. "skills": skills,
  1327. }
  1328. api_base = _knowhub_api()
  1329. timeout = _remote_agent_timeout()
  1330. try:
  1331. async with httpx.AsyncClient(timeout=timeout) as client:
  1332. response = await client.post(f"{api_base}/api/agent", json=payload)
  1333. response.raise_for_status()
  1334. result = response.json()
  1335. return {
  1336. "mode": "remote",
  1337. "agent_type": agent_type,
  1338. "sub_trace_id": result.get("sub_trace_id"),
  1339. "status": result.get("status", "completed"),
  1340. "summary": result.get("summary", ""),
  1341. "stats": result.get("stats", {}),
  1342. "error": result.get("error"),
  1343. }
  1344. except httpx.HTTPStatusError as e:
  1345. return {
  1346. "mode": "remote",
  1347. "agent_type": agent_type,
  1348. "status": "failed",
  1349. "error": f"HTTP {e.response.status_code}: {e.response.text[:200]}",
  1350. }
  1351. except Exception as e:
  1352. return {
  1353. "mode": "remote",
  1354. "agent_type": agent_type,
  1355. "status": "failed",
  1356. "error": f"远端调用失败: {type(e).__name__}: {e}",
  1357. }
  1358. # ===== 工具定义 =====
  1359. @tool(description="创建子 Agent 执行任务;Recursive revision 2 仅允许本地结构化委派", hidden_params=["context"], groups=["core"])
  1360. async def agent(
  1361. task: Optional[Union[str, List[str]]] = None,
  1362. task_brief: Optional[Union[TaskBrief, List[TaskBrief]]] = None,
  1363. messages: Optional[Union[Messages, List[Messages]]] = None,
  1364. continue_from: Optional[str] = None,
  1365. agent_type: Optional[str] = None,
  1366. skills: Optional[List[str]] = None,
  1367. context: Optional[dict] = None,
  1368. ) -> Dict[str, Any]:
  1369. """
  1370. 父 Agent 用来创建或续跑直属子 Agent 的公开工具。
  1371. 路由规则:
  1372. - Legacy/Recursive revision 1 中 agent_type 以 "remote_" 开头:HTTP
  1373. 调用 KnowHub 服务器的 /api/agent(仅单任务,无本地文件访问)
  1374. - Recursive revision 2 不支持 remote Agent,也不会发出 HTTP 请求
  1375. - 否则本地执行:Legacy/Recursive revision 1 使用 ``task``;revision 2 使用 ``task_brief``
  1376. Args:
  1377. task: Legacy/Recursive revision 1 任务描述。
  1378. task_brief: Recursive revision 2 的结构化任务说明。
  1379. messages: 预置消息。1D 列表=所有 agent 共享;2D 列表=per-agent
  1380. continue_from: 继续已有 trace(仅单任务)
  1381. agent_type: 子 Agent 类型。Legacy/Recursive revision 1 带
  1382. "remote_" 前缀走远端;否则本地 preset
  1383. skills: 指定本次调用使用的 skill 列表
  1384. - 本地:附加到 system prompt
  1385. - 远端:原样发送,由远端实现决定最终权限
  1386. context: 框架自动注入的上下文
  1387. """
  1388. try:
  1389. assert_removed_config_absent()
  1390. except ValueError as exc:
  1391. return {"status": "failed", "error": str(exc)}
  1392. # 任何路由决策都必须来自已持久化的父 Trace 策略。先看
  1393. # ``remote_*`` 会让 Recursive revision 2 绕过本地协议、预算与审核门禁。
  1394. if not context:
  1395. return {"status": "failed", "error": "context is required"}
  1396. store = context.get("store")
  1397. trace_id = context.get("trace_id")
  1398. goal_id = context.get("goal_id")
  1399. runner = context.get("runner")
  1400. missing = []
  1401. if not store:
  1402. missing.append("store")
  1403. if not trace_id:
  1404. missing.append("trace_id")
  1405. if not runner:
  1406. missing.append("runner")
  1407. if missing:
  1408. return {"status": "failed", "error": f"Missing required context: {', '.join(missing)}"}
  1409. parent_trace = await store.get_trace(trace_id)
  1410. if not parent_trace:
  1411. return {"status": "failed", "error": f"Parent trace not found: {trace_id}"}
  1412. try:
  1413. policy = policy_from_context(parent_trace.context)
  1414. except ValueError as exc:
  1415. return {"status": "failed", "error": str(exc)}
  1416. if isinstance(messages, str):
  1417. try:
  1418. messages = json.loads(messages)
  1419. except json.JSONDecodeError as exc:
  1420. return {"status": "failed", "error": f"Invalid messages JSON: {exc}"}
  1421. if messages is not None:
  1422. is_1d = isinstance(messages, list) and all(
  1423. isinstance(message, dict) for message in messages
  1424. )
  1425. is_2d = isinstance(messages, list) and bool(messages) and all(
  1426. isinstance(message_list, list)
  1427. and all(isinstance(message, dict) for message in message_list)
  1428. for message_list in messages
  1429. )
  1430. if not (is_1d or is_2d):
  1431. return {
  1432. "status": "failed",
  1433. "error": "messages must be a 1D or 2D message list without mixed dimensions",
  1434. }
  1435. # 远端路由:agent_type 以 remote_ 开头
  1436. if agent_type and agent_type.startswith(REMOTE_PREFIX):
  1437. if policy.requires_task_protocol:
  1438. return {
  1439. "status": "failed",
  1440. "error": (
  1441. "Recursive revision 2 does not support remote agents; "
  1442. "use a local task_brief delegation"
  1443. ),
  1444. }
  1445. if not isinstance(task, str):
  1446. return {"status": "failed", "error": "remote agent 只支持单任务 (task: str)"}
  1447. # 归一化 messages:远端只接受 1D Messages 或 None
  1448. remote_msgs: Optional[Messages] = None
  1449. if messages is not None:
  1450. if messages and isinstance(messages[0], list):
  1451. return {"status": "failed", "error": "remote agent 不支持 2D messages (per-agent)"}
  1452. remote_msgs = messages
  1453. return await _run_remote_agent(
  1454. agent_type=agent_type,
  1455. task=task,
  1456. messages=remote_msgs,
  1457. continue_from=continue_from,
  1458. skills=skills,
  1459. )
  1460. if policy.requires_task_protocol and messages is not None:
  1461. return {
  1462. "status": "failed",
  1463. "error": "Recursive revision 2 does not accept messages; pass bounded context in task_brief",
  1464. }
  1465. parsed_task_briefs: Optional[List[TaskBrief]] = None
  1466. if policy.requires_task_protocol:
  1467. if task is not None or task_brief is None:
  1468. return {
  1469. "status": "failed",
  1470. "error": "Recursive revision 2 requires task_brief and does not accept task",
  1471. }
  1472. if isinstance(task_brief, str):
  1473. try:
  1474. task_brief = json.loads(task_brief)
  1475. except json.JSONDecodeError as exc:
  1476. return {"status": "failed", "error": f"Invalid TaskBrief JSON: {exc}"}
  1477. raw_briefs = task_brief if isinstance(task_brief, list) else [task_brief]
  1478. try:
  1479. _, root_task_anchor = await _load_root_task_anchor(
  1480. store,
  1481. parent_trace,
  1482. )
  1483. parent_state = ensure_task_protocol(parent_trace.context)
  1484. parent_task_brief = parent_state.get("task_brief")
  1485. parsed_task_briefs = [
  1486. normalize_task_brief(
  1487. item,
  1488. parent_task_brief=parent_task_brief,
  1489. root_task_anchor=root_task_anchor,
  1490. )
  1491. for item in raw_briefs
  1492. ]
  1493. except (ContextPolicyError, ValidationError) as exc:
  1494. return {"status": "failed", "error": f"Invalid TaskBrief: {exc}"}
  1495. tasks = [brief.objective for brief in parsed_task_briefs]
  1496. single = len(tasks) == 1
  1497. else:
  1498. if task_brief is not None:
  1499. return {"status": "failed", "error": "task_brief requires Recursive revision 2"}
  1500. if task is None:
  1501. return {"status": "failed", "error": "task is required"}
  1502. # 归一化 task → list(保留 Legacy 字符串 JSON 列表兼容)
  1503. if isinstance(task, str):
  1504. task_str = task.strip()
  1505. if task_str.startswith("[") and task_str.endswith("]"):
  1506. try:
  1507. parsed_task = json.loads(task_str)
  1508. if isinstance(parsed_task, list):
  1509. task = parsed_task
  1510. except (json.JSONDecodeError, TypeError):
  1511. pass
  1512. single = isinstance(task, str)
  1513. tasks = [task] if single else task
  1514. if not tasks:
  1515. return {"status": "failed", "error": "task is required"}
  1516. # 归一化 messages → List[Messages](per-agent)
  1517. if messages is None:
  1518. per_agent_msgs: List[Messages] = [[] for _ in tasks]
  1519. elif messages and isinstance(messages[0], list):
  1520. if len(messages) != len(tasks):
  1521. return {
  1522. "status": "failed",
  1523. "error": (
  1524. "2D messages must contain exactly one message list per task: "
  1525. f"tasks={len(tasks)}, messages={len(messages)}"
  1526. ),
  1527. }
  1528. per_agent_msgs = messages # 2D: per-agent
  1529. else:
  1530. per_agent_msgs = [messages] * len(tasks) # 1D: 共享
  1531. if continue_from and not single:
  1532. return {"status": "failed", "error": "continue_from requires single task"}
  1533. return await _run_agents(
  1534. tasks, per_agent_msgs, continue_from,
  1535. store, trace_id, goal_id, runner, context,
  1536. agent_type=agent_type,
  1537. skills=skills,
  1538. task_briefs=parsed_task_briefs,
  1539. )
  1540. @tool(description="评估目标执行结果是否满足要求", hidden_params=["context"], groups=["core"])
  1541. async def evaluate(
  1542. messages: Optional[Messages] = None,
  1543. target_goal_id: Optional[str] = None,
  1544. continue_from: Optional[str] = None,
  1545. context: Optional[dict] = None,
  1546. ) -> Dict[str, Any]:
  1547. """
  1548. 评估目标执行结果是否满足要求。
  1549. 代码自动从 GoalTree 注入目标描述。模型把执行结果和上下文放在 messages 中。
  1550. Args:
  1551. messages: 执行结果和上下文消息(OpenAI 格式)
  1552. target_goal_id: 要评估的目标 ID(默认当前 goal_id)
  1553. continue_from: 继续已有评估 trace
  1554. context: 框架自动注入的上下文
  1555. """
  1556. if not context:
  1557. return {"status": "failed", "error": "context is required"}
  1558. store = context.get("store")
  1559. trace_id = context.get("trace_id")
  1560. current_goal_id = context.get("goal_id")
  1561. runner = context.get("runner")
  1562. missing = []
  1563. if not store:
  1564. missing.append("store")
  1565. if not trace_id:
  1566. missing.append("trace_id")
  1567. if not runner:
  1568. missing.append("runner")
  1569. if missing:
  1570. return {"status": "failed", "error": f"Missing required context: {', '.join(missing)}"}
  1571. # target_goal_id 默认 context["goal_id"]
  1572. goal_id = target_goal_id or current_goal_id
  1573. # 从 GoalTree 获取目标描述
  1574. goal_desc = await _get_goal_description(store, trace_id, goal_id)
  1575. # 构建 evaluator prompt
  1576. eval_prompt = _build_evaluate_prompt(goal_desc, messages)
  1577. # 获取父 Trace 信息
  1578. parent_trace = await store.get_trace(trace_id)
  1579. if not parent_trace:
  1580. return {"status": "failed", "error": f"Parent trace not found: {trace_id}"}
  1581. try:
  1582. policy = policy_from_context(parent_trace.context)
  1583. except ValueError as exc:
  1584. return {"status": "failed", "error": str(exc)}
  1585. if policy.requires_task_protocol:
  1586. return {
  1587. "status": "failed",
  1588. "error": "evaluate is unavailable in Recursive mode; validation is framework-managed",
  1589. }
  1590. # 处理 continue_from 或创建新 Sub-Trace
  1591. if continue_from:
  1592. existing_trace = await store.get_trace(continue_from)
  1593. if not existing_trace:
  1594. return {"status": "failed", "error": f"Continue-from trace not found: {continue_from}"}
  1595. if existing_trace.parent_trace_id != trace_id:
  1596. return {
  1597. "status": "failed",
  1598. "error": "continue_from must reference a direct child of the current trace",
  1599. }
  1600. if existing_trace.uid != parent_trace.uid:
  1601. return {
  1602. "status": "failed",
  1603. "error": "continue_from trace owner does not match the current trace owner",
  1604. }
  1605. if existing_trace.context.get("created_by_tool") != "evaluate":
  1606. return {
  1607. "status": "failed",
  1608. "error": "continue_from must reference a child created by evaluate",
  1609. }
  1610. try:
  1611. existing_policy = policy_from_context(existing_trace.context)
  1612. except ValueError as exc:
  1613. return {"status": "failed", "error": str(exc)}
  1614. if (
  1615. existing_policy.mode is not policy.mode
  1616. or existing_policy.revision != policy.revision
  1617. ):
  1618. return {
  1619. "status": "failed",
  1620. "error": "continue_from trace Agent mode does not match the current trace",
  1621. }
  1622. parent_depth, expected_root_trace_id = await _resolve_trace_lineage(
  1623. store, parent_trace
  1624. )
  1625. child_depth, root_trace_id = await _resolve_trace_lineage(
  1626. store, existing_trace
  1627. )
  1628. if (
  1629. child_depth != parent_depth + 1
  1630. or root_trace_id != expected_root_trace_id
  1631. ):
  1632. return {
  1633. "status": "failed",
  1634. "error": "continue_from trace lineage does not match the current trace",
  1635. }
  1636. sub_trace_id = continue_from
  1637. goal_tree = await store.get_goal_tree(continue_from)
  1638. mission = goal_tree.mission if goal_tree else eval_prompt
  1639. sub_trace_ids = [{"trace_id": sub_trace_id, "mission": mission}]
  1640. else:
  1641. sub_trace_id = generate_sub_trace_id(trace_id, "evaluate")
  1642. parent_depth, root_trace_id = await _resolve_trace_lineage(store, parent_trace)
  1643. evaluator_context = apply_policy_to_context({
  1644. "created_by_tool": "evaluate",
  1645. "agent_depth": parent_depth + 1,
  1646. "root_trace_id": root_trace_id,
  1647. }, policy)
  1648. sub_trace = Trace(
  1649. trace_id=sub_trace_id,
  1650. mode="agent",
  1651. task=eval_prompt,
  1652. parent_trace_id=trace_id,
  1653. parent_goal_id=current_goal_id,
  1654. agent_type="evaluate",
  1655. uid=parent_trace.uid if parent_trace else None,
  1656. model=parent_trace.model if parent_trace else None,
  1657. status="running",
  1658. context=evaluator_context,
  1659. created_at=datetime.now(),
  1660. )
  1661. await store.create_trace(sub_trace)
  1662. await store.update_goal_tree(sub_trace_id, GoalTree(mission=eval_prompt))
  1663. sub_trace_ids = [{"trace_id": sub_trace_id, "mission": eval_prompt}]
  1664. # 广播 sub_trace_started
  1665. await broadcast_sub_trace_started(
  1666. trace_id, sub_trace_id, current_goal_id or "",
  1667. "evaluate", eval_prompt,
  1668. )
  1669. # 更新主 Goal 为 in_progress
  1670. await _update_goal_start(
  1671. store,
  1672. trace_id,
  1673. current_goal_id,
  1674. "evaluate",
  1675. sub_trace_ids,
  1676. accumulate_sub_trace_ids=policy.accumulate_sub_trace_ids,
  1677. )
  1678. # 注册为活跃协作者
  1679. eval_name = f"评估: {(goal_id or 'unknown')[:20]}"
  1680. await _update_collaborator(
  1681. store, trace_id,
  1682. name=eval_name, sub_trace_id=sub_trace_id,
  1683. status="running", summary=f"评估 Goal {goal_id}",
  1684. )
  1685. # 执行评估
  1686. try:
  1687. # evaluate 使用只读工具 + goal
  1688. allowed_tools = ["read_file", "grep_content", "glob_files", "goal"]
  1689. result = await runner.run_result(
  1690. messages=[{"role": "user", "content": eval_prompt}],
  1691. config=_make_run_config(
  1692. trace_id=sub_trace_id,
  1693. agent_type="evaluate",
  1694. model=parent_trace.model if parent_trace else "gpt-4o",
  1695. uid=parent_trace.uid if parent_trace else None,
  1696. tools=allowed_tools,
  1697. tool_groups=[],
  1698. exclude_tools=["agent", "evaluate", "bash_command"],
  1699. name=f"评估: {goal_id}",
  1700. ),
  1701. on_event=_make_interactive_handler(
  1702. runner, sub_trace_id, trace_id,
  1703. debug_printer=_make_event_printer("evaluate") if getattr(runner, 'debug', False) else None,
  1704. ),
  1705. )
  1706. await broadcast_sub_trace_completed(
  1707. trace_id, sub_trace_id,
  1708. result.get("status", "completed"),
  1709. result.get("summary", ""),
  1710. result.get("stats", {}),
  1711. )
  1712. await _update_collaborator(
  1713. store, trace_id,
  1714. name=eval_name, sub_trace_id=sub_trace_id,
  1715. status=result.get("status", "completed"),
  1716. summary=result.get("summary", "")[:80],
  1717. )
  1718. formatted_summary = result.get("summary", "")
  1719. await _update_goal_complete(
  1720. store, trace_id, current_goal_id,
  1721. result.get("status", "completed"),
  1722. formatted_summary,
  1723. )
  1724. return {
  1725. "mode": "evaluate",
  1726. "sub_trace_id": sub_trace_id,
  1727. "continue_from": bool(continue_from),
  1728. **result,
  1729. "summary": formatted_summary,
  1730. }
  1731. except Exception as e:
  1732. error_msg = str(e)
  1733. await broadcast_sub_trace_completed(
  1734. trace_id, sub_trace_id, "failed", error_msg, {},
  1735. )
  1736. await _update_collaborator(
  1737. store, trace_id,
  1738. name=eval_name, sub_trace_id=sub_trace_id,
  1739. status="failed", summary=error_msg[:80],
  1740. )
  1741. await _update_goal_complete(
  1742. store, trace_id, current_goal_id,
  1743. "failed", f"评估任务失败: {error_msg}",
  1744. )
  1745. return {
  1746. "mode": "evaluate",
  1747. "status": "failed",
  1748. "error": error_msg,
  1749. "sub_trace_id": sub_trace_id,
  1750. }