subagent.py 86 KB

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