subagent.py 71 KB

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