subagent.py 59 KB

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