subagent.py 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116
  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 os
  10. from datetime import datetime
  11. from typing import Any, Dict, List, Optional, Union
  12. from cyber_agent.tools import tool
  13. from cyber_agent.trace.models import Trace, Messages
  14. from cyber_agent.trace.trace_id import generate_sub_trace_id
  15. from cyber_agent.trace.goal_models import GoalTree
  16. from cyber_agent.trace.websocket import broadcast_sub_trace_started, broadcast_sub_trace_completed
  17. # ===== 远端路由常量 =====
  18. REMOTE_PREFIX = "remote_"
  19. MAX_AGENT_DEPTH = 5
  20. MAX_LOCAL_CHILDREN_PER_TRACE = 6
  21. # POC 阶段使用进程内锁保护“检查配额 -> 创建 Trace”。
  22. # 多 worker 部署时需要换成跨进程的原子配额。
  23. _LOCAL_AGENT_SPAWN_LOCK = asyncio.Lock()
  24. def _knowhub_api() -> str:
  25. """运行时读取 KNOWHUB_API,避免 module-load 时 .env 尚未加载的情况"""
  26. return os.getenv("KNOWHUB_API", "http://localhost:9999").rstrip("/")
  27. def _remote_agent_timeout() -> float:
  28. return float(os.getenv("REMOTE_AGENT_TIMEOUT", "600"))
  29. def _recursion_enabled() -> bool:
  30. """是否允许本地 Sub-Agent 继续创建 Sub-Agent。"""
  31. return os.getenv("AGENT_RECURSION_ENABLED", "false").strip().lower() in {
  32. "1", "true", "yes", "on",
  33. }
  34. # 兼容旧代码对 module-level 常量的引用(运行时值 = 首次 import 时的快照)
  35. KNOWHUB_API = _knowhub_api()
  36. REMOTE_AGENT_TIMEOUT = _remote_agent_timeout()
  37. # ===== prompts =====
  38. # ===== 评估任务 =====
  39. EVALUATE_PROMPT_TEMPLATE = """# 评估任务
  40. 请评估以下任务的执行结果是否满足要求。
  41. ## 目标描述
  42. {goal_description}
  43. ## 执行结果
  44. {result_text}
  45. ## 输出格式
  46. ## 评估结论
  47. [通过/不通过]
  48. ## 评估理由
  49. [详细说明通过或不通过原因]
  50. ## 修改建议(如果不通过)
  51. 1. [建议1]
  52. 2. [建议2]
  53. """
  54. # ===== 结果格式化 =====
  55. DELEGATE_RESULT_HEADER = "## 委托任务完成\n"
  56. DELEGATE_SAVED_KNOWLEDGE_HEADER = "**保存的知识** ({count} 条):"
  57. DELEGATE_STATS_HEADER = "**执行统计**:"
  58. EXPLORE_RESULT_HEADER = "## 探索结果\n"
  59. EXPLORE_BRANCH_TEMPLATE = "### 方案 {branch_name}: {task}"
  60. EXPLORE_STATUS_SUCCESS = "**状态**: ✓ 完成"
  61. EXPLORE_STATUS_FAILED = "**状态**: ✗ 失败"
  62. EXPLORE_STATUS_ERROR = "**状态**: ✗ 异常"
  63. EXPLORE_SUMMARY_HEADER = "## 总结"
  64. def build_evaluate_prompt(goal_description: str, result_text: str) -> str:
  65. return EVALUATE_PROMPT_TEMPLATE.format(
  66. goal_description=goal_description,
  67. result_text=result_text or "(无执行结果)",
  68. )
  69. def _make_run_config(**kwargs):
  70. """延迟导入 RunConfig 以避免循环导入"""
  71. from cyber_agent.core.runner import RunConfig
  72. return RunConfig(**kwargs)
  73. # ===== 辅助函数 =====
  74. async def _update_collaborator(
  75. store, trace_id: str,
  76. name: str, sub_trace_id: str,
  77. status: str, summary: str = "",
  78. ) -> None:
  79. """
  80. 更新 trace.context["collaborators"] 中的协作者信息。
  81. 如果同名协作者已存在则更新,否则追加。
  82. """
  83. trace = await store.get_trace(trace_id)
  84. if not trace:
  85. return
  86. collaborators = trace.context.get("collaborators", [])
  87. # 查找已有记录
  88. existing = None
  89. for c in collaborators:
  90. if c.get("trace_id") == sub_trace_id:
  91. existing = c
  92. break
  93. if existing:
  94. existing["status"] = status
  95. if summary:
  96. existing["summary"] = summary
  97. else:
  98. collaborators.append({
  99. "name": name,
  100. "type": "agent",
  101. "trace_id": sub_trace_id,
  102. "status": status,
  103. "summary": summary,
  104. })
  105. trace.context["collaborators"] = collaborators
  106. await store.update_trace(trace_id, context=trace.context)
  107. async def _update_goal_start(
  108. store, trace_id: str, goal_id: str, mode: str,
  109. sub_trace_ids: List[Dict[str, str]],
  110. ) -> None:
  111. """标记 Goal 开始执行"""
  112. if not goal_id:
  113. return
  114. tree = await store.get_goal_tree(trace_id)
  115. goal = tree.find(goal_id) if tree else None
  116. existing_entries = goal.sub_trace_ids if goal and goal.sub_trace_ids else []
  117. merged_entries: Dict[str, Dict[str, str]] = {}
  118. for entry in [*existing_entries, *sub_trace_ids]:
  119. if isinstance(entry, str):
  120. merged_entries[entry] = {"trace_id": entry, "mission": entry}
  121. elif isinstance(entry, dict) and entry.get("trace_id"):
  122. merged_entries[entry["trace_id"]] = entry
  123. await store.update_goal(
  124. trace_id, goal_id,
  125. type="agent_call",
  126. agent_call_mode=mode,
  127. status="in_progress",
  128. sub_trace_ids=list(merged_entries.values()),
  129. )
  130. async def _update_goal_complete(
  131. store, trace_id: str, goal_id: str,
  132. status: str, summary: str,
  133. ) -> None:
  134. """标记 Goal 完成"""
  135. if not goal_id:
  136. return
  137. await store.update_goal(
  138. trace_id, goal_id,
  139. status=status,
  140. summary=summary,
  141. )
  142. def _aggregate_stats(results: List[Dict[str, Any]]) -> Dict[str, Any]:
  143. """聚合多个结果的统计信息"""
  144. total_messages = 0
  145. total_tokens = 0
  146. total_cost = 0.0
  147. for result in results:
  148. if isinstance(result, dict) and "stats" in result:
  149. stats = result["stats"]
  150. total_messages += stats.get("total_messages", 0)
  151. total_tokens += stats.get("total_tokens", 0)
  152. total_cost += stats.get("total_cost", 0.0)
  153. return {
  154. "total_messages": total_messages,
  155. "total_tokens": total_tokens,
  156. "total_cost": total_cost
  157. }
  158. def _get_allowed_tools(context: dict, agent_depth: int) -> Optional[List[str]]:
  159. """按子 Agent 深度生成唯一的工具权限列表。"""
  160. runner = context.get("runner")
  161. if runner and hasattr(runner, "tools") and hasattr(runner.tools, "get_tool_names"):
  162. blocked_tools = {"evaluate", "bash_command"}
  163. if not _recursion_enabled() or agent_depth >= MAX_AGENT_DEPTH:
  164. blocked_tools.add("agent")
  165. return [
  166. name for name in runner.tools.get_tool_names()
  167. if name not in blocked_tools
  168. ]
  169. return None
  170. async def _resolve_trace_lineage(store, trace: Trace) -> tuple[int, str]:
  171. """返回 Trace 的 (agent_depth, root_trace_id)。
  172. 新 Trace 直接读 context;旧 Trace 缺少字段时沿 parent_trace_id
  173. 回溯,不依赖 Trace ID 字符串格式。
  174. """
  175. depth = trace.context.get("agent_depth")
  176. root_trace_id = trace.context.get("root_trace_id")
  177. if isinstance(depth, int) and depth >= 0 and isinstance(root_trace_id, str):
  178. return depth, root_trace_id
  179. depth = 0
  180. current = trace
  181. root_trace_id = trace.trace_id
  182. visited = {trace.trace_id}
  183. while current.parent_trace_id:
  184. parent_id = current.parent_trace_id
  185. if parent_id in visited:
  186. break
  187. visited.add(parent_id)
  188. parent = await store.get_trace(parent_id)
  189. if not parent:
  190. break
  191. depth += 1
  192. root_trace_id = parent.trace_id
  193. current = parent
  194. return depth, root_trace_id
  195. def _format_single_result(result: Dict[str, Any], sub_trace_id: str, continued: bool) -> Dict[str, Any]:
  196. """格式化单任务(delegate)结果"""
  197. lines = [DELEGATE_RESULT_HEADER]
  198. summary = result.get("summary", "")
  199. if summary:
  200. lines.append(summary)
  201. lines.append("")
  202. # 添加保存的知识 ID
  203. saved_knowledge_ids = result.get("saved_knowledge_ids", [])
  204. if saved_knowledge_ids:
  205. lines.append("---\n")
  206. lines.append(DELEGATE_SAVED_KNOWLEDGE_HEADER.format(count=len(saved_knowledge_ids)))
  207. for kid in saved_knowledge_ids:
  208. lines.append(f"- {kid}")
  209. lines.append("")
  210. lines.append("---\n")
  211. lines.append(DELEGATE_STATS_HEADER)
  212. stats = result.get("stats", {})
  213. if stats:
  214. lines.append(f"- 消息数: {stats.get('total_messages', 0)}")
  215. lines.append(f"- Tokens: {stats.get('total_tokens', 0)}")
  216. lines.append(f"- 成本: ${stats.get('total_cost', 0.0):.4f}")
  217. formatted_summary = "\n".join(lines)
  218. return {
  219. "mode": "delegate",
  220. "sub_trace_id": sub_trace_id,
  221. "continue_from": continued,
  222. "saved_knowledge_ids": saved_knowledge_ids, # 传递给父 agent
  223. **result,
  224. "summary": formatted_summary,
  225. }
  226. def _format_multi_result(
  227. tasks: List[str], results: List[Dict[str, Any]], sub_trace_ids: List[Dict]
  228. ) -> Dict[str, Any]:
  229. """格式化多任务(explore)聚合结果"""
  230. lines = [EXPLORE_RESULT_HEADER]
  231. successful = 0
  232. failed = 0
  233. total_tokens = 0
  234. total_cost = 0.0
  235. for i, (task_item, result) in enumerate(zip(tasks, results)):
  236. branch_name = chr(ord('A') + i)
  237. lines.append(EXPLORE_BRANCH_TEMPLATE.format(branch_name=branch_name, task=task_item))
  238. if isinstance(result, dict):
  239. status = result.get("status", "unknown")
  240. if status == "completed":
  241. lines.append(EXPLORE_STATUS_SUCCESS)
  242. successful += 1
  243. else:
  244. lines.append(EXPLORE_STATUS_FAILED)
  245. failed += 1
  246. summary = result.get("summary", "")
  247. if summary:
  248. lines.append(f"**摘要**: {summary[:200]}...")
  249. stats = result.get("stats", {})
  250. if stats:
  251. messages = stats.get("total_messages", 0)
  252. tokens = stats.get("total_tokens", 0)
  253. cost = stats.get("total_cost", 0.0)
  254. lines.append(f"**统计**: {messages} messages, {tokens} tokens, ${cost:.4f}")
  255. total_tokens += tokens
  256. total_cost += cost
  257. else:
  258. lines.append(EXPLORE_STATUS_ERROR)
  259. failed += 1
  260. lines.append("")
  261. lines.append("---\n")
  262. lines.append(EXPLORE_SUMMARY_HEADER)
  263. lines.append(f"- 总分支数: {len(tasks)}")
  264. lines.append(f"- 成功: {successful}")
  265. lines.append(f"- 失败: {failed}")
  266. lines.append(f"- 总 tokens: {total_tokens}")
  267. lines.append(f"- 总成本: ${total_cost:.4f}")
  268. aggregated_summary = "\n".join(lines)
  269. overall_status = "completed" if successful > 0 else "failed"
  270. return {
  271. "mode": "explore",
  272. "status": overall_status,
  273. "summary": aggregated_summary,
  274. "sub_trace_ids": sub_trace_ids,
  275. "tasks": tasks,
  276. "stats": _aggregate_stats(results),
  277. }
  278. async def _get_goal_description(store, trace_id: str, goal_id: str) -> str:
  279. """从 GoalTree 获取目标描述"""
  280. if not goal_id:
  281. return ""
  282. goal_tree = await store.get_goal_tree(trace_id)
  283. if goal_tree:
  284. target_goal = goal_tree.find(goal_id)
  285. if target_goal:
  286. return target_goal.description
  287. return f"Goal {goal_id}"
  288. def _build_evaluate_prompt(goal_description: str, messages: Optional[Messages]) -> str:
  289. """
  290. 构建评估 prompt。
  291. Args:
  292. goal_description: 代码从 GoalTree 注入的目标描述
  293. messages: 模型提供的消息(执行结果+上下文)
  294. """
  295. # 从 messages 提取文本内容
  296. result_text = ""
  297. if messages:
  298. parts = []
  299. for msg in messages:
  300. content = msg.get("content", "")
  301. if isinstance(content, str):
  302. parts.append(content)
  303. elif isinstance(content, list):
  304. # 多模态内容,提取文本部分
  305. for item in content:
  306. if isinstance(item, dict) and item.get("type") == "text":
  307. parts.append(item.get("text", ""))
  308. result_text = "\n".join(parts)
  309. return build_evaluate_prompt(goal_description, result_text)
  310. def _make_event_printer(label: str):
  311. """
  312. 创建子 Agent 执行过程打印函数。
  313. 当父 runner.debug=True 时,传给 run_result(on_event=...),
  314. 实时输出子 Agent 的工具调用和助手消息。
  315. """
  316. prefix = f" [{label}]"
  317. def on_event(item):
  318. from cyber_agent.trace.models import Trace, Message
  319. if isinstance(item, Message):
  320. if item.role == "assistant":
  321. content = item.content
  322. if isinstance(content, dict):
  323. text = content.get("text", "")
  324. tool_calls = content.get("tool_calls")
  325. if text:
  326. preview = text[:120] + "..." if len(text) > 120 else text
  327. print(f"{prefix} {preview}")
  328. if tool_calls:
  329. for tc in tool_calls:
  330. name = tc.get("function", {}).get("name", "unknown")
  331. print(f"{prefix} 🛠️ {name}")
  332. elif item.role == "tool":
  333. content = item.content
  334. if isinstance(content, dict):
  335. name = content.get("tool_name", "unknown")
  336. desc = item.description or ""
  337. desc_short = (desc[:60] + "...") if len(desc) > 60 else desc
  338. suffix = f": {desc_short}" if desc_short else ""
  339. print(f"{prefix} ✅ {name}{suffix}")
  340. elif isinstance(item, Trace):
  341. if item.status == "completed":
  342. print(f"{prefix} ✓ 完成")
  343. elif item.status == "failed":
  344. err = (item.error_message or "")[:80]
  345. print(f"{prefix} ✗ 失败: {err}")
  346. return on_event
  347. def _make_interactive_handler(runner, sub_trace_id: str, parent_trace_id: str, debug_printer=None):
  348. """
  349. 创建支持 stdin 交互检查的 on_event 回调。
  350. 在每个子 Agent 事件触发时检查 stdin,检测到暂停/退出信号后
  351. 通过 cancel_event.set() 停止子 agent 和父 agent 的执行。
  352. """
  353. def on_event(item):
  354. # 先执行 debug 打印
  355. if debug_printer:
  356. debug_printer(item)
  357. # 检查 stdin
  358. check_fn = getattr(runner, 'stdin_check', None)
  359. if not check_fn:
  360. return
  361. cmd = check_fn()
  362. if cmd in ('pause', 'quit'):
  363. # cancel_event.set() 是同步操作,可以在同步回调中直接调用
  364. for tid in (sub_trace_id, parent_trace_id):
  365. ev = runner._cancel_events.get(tid)
  366. if ev:
  367. ev.set()
  368. return on_event
  369. # ===== 统一内部执行函数 =====
  370. async def _run_agents(
  371. tasks: List[str],
  372. per_agent_msgs: List[Messages],
  373. continue_from: Optional[str],
  374. store, trace_id: str, goal_id: str, runner, context: dict,
  375. agent_type: Optional[str] = None,
  376. skills: Optional[List[str]] = None,
  377. ) -> Dict[str, Any]:
  378. """
  379. 统一 agent 执行逻辑。
  380. single (len(tasks)==1): delegate 模式,全量工具
  381. multi (len(tasks)>1): explore 模式,并行执行
  382. AGENT_RECURSION_ENABLED=true 时,depth < MAX_AGENT_DEPTH 的本地
  383. Sub-Agent 可继续使用 agent 工具。
  384. """
  385. single = len(tasks) == 1
  386. parent_trace = await store.get_trace(trace_id)
  387. if not parent_trace:
  388. return {"status": "failed", "error": f"Parent trace not found: {trace_id}"}
  389. # continue_from: 复用已有 trace(仅 single)
  390. sub_trace_id = None
  391. continued = False
  392. goal_started = False
  393. child_records: List[Dict[str, Any]] = []
  394. all_sub_trace_ids: List[Dict[str, str]] = []
  395. if single and continue_from:
  396. existing = await store.get_trace(continue_from)
  397. if not existing:
  398. return {"status": "failed", "error": f"Continue-from trace not found: {continue_from}"}
  399. if existing.parent_trace_id != trace_id:
  400. return {
  401. "status": "failed",
  402. "error": "continue_from must reference a direct child of the current trace",
  403. }
  404. if existing.uid != parent_trace.uid:
  405. return {
  406. "status": "failed",
  407. "error": "continue_from trace owner does not match the current trace owner",
  408. }
  409. sub_trace_id = continue_from
  410. continued = True
  411. goal_tree = await store.get_goal_tree(continue_from)
  412. mission = goal_tree.mission if goal_tree else tasks[0]
  413. child_depth, root_trace_id = await _resolve_trace_lineage(store, existing)
  414. all_sub_trace_ids = [{"trace_id": sub_trace_id, "mission": mission}]
  415. child_records = [{
  416. "trace_id": sub_trace_id,
  417. "depth": child_depth,
  418. "context": {
  419. **existing.context,
  420. "agent_depth": child_depth,
  421. "root_trace_id": root_trace_id,
  422. },
  423. "is_new": False,
  424. }]
  425. else:
  426. parent_depth, root_trace_id = await _resolve_trace_lineage(store, parent_trace)
  427. if parent_depth > 0 and not _recursion_enabled():
  428. return {
  429. "status": "failed",
  430. "error": "Local Sub-Agent recursion is disabled",
  431. }
  432. if parent_depth >= MAX_AGENT_DEPTH:
  433. return {
  434. "status": "failed",
  435. "error": (
  436. f"Local Sub-Agent depth limit reached: "
  437. f"depth={parent_depth}, max={MAX_AGENT_DEPTH}"
  438. ),
  439. }
  440. # 将配额检查和 Trace 创建放在同一进程内临界区,
  441. # 保证两个并发 agent() 调用不会共同突破 6 个直接孩子。
  442. async with _LOCAL_AGENT_SPAWN_LOCK:
  443. existing_children = await store.list_traces(
  444. parent_trace_id=trace_id,
  445. created_by_tool="agent",
  446. limit=MAX_LOCAL_CHILDREN_PER_TRACE + 1,
  447. )
  448. requested_children = len(tasks)
  449. if len(existing_children) + requested_children > MAX_LOCAL_CHILDREN_PER_TRACE:
  450. return {
  451. "status": "failed",
  452. "error": (
  453. f"Local child Agent limit exceeded: existing={len(existing_children)}, "
  454. f"requested={requested_children}, "
  455. f"max={MAX_LOCAL_CHILDREN_PER_TRACE}"
  456. ),
  457. }
  458. child_depth = parent_depth + 1
  459. for i, task_item in enumerate(tasks):
  460. resolved_agent_type = agent_type or ("delegate" if single else "explore")
  461. suffix = "delegate" if single else f"explore-{i+1:03d}"
  462. stid = generate_sub_trace_id(trace_id, suffix)
  463. child_context = {
  464. "created_by_tool": "agent",
  465. "agent_depth": child_depth,
  466. "root_trace_id": root_trace_id,
  467. }
  468. sub_trace = Trace(
  469. trace_id=stid,
  470. mode="agent",
  471. task=task_item,
  472. parent_trace_id=trace_id,
  473. parent_goal_id=goal_id,
  474. agent_type=resolved_agent_type,
  475. uid=parent_trace.uid,
  476. model=parent_trace.model,
  477. status="running",
  478. context=child_context,
  479. created_at=datetime.now(),
  480. )
  481. await store.create_trace(sub_trace)
  482. await store.update_goal_tree(stid, GoalTree(mission=task_item))
  483. all_sub_trace_ids.append({"trace_id": stid, "mission": task_item})
  484. child_records.append({
  485. "trace_id": stid,
  486. "depth": child_depth,
  487. "context": child_context,
  488. "is_new": True,
  489. "agent_type": resolved_agent_type,
  490. })
  491. if single:
  492. sub_trace_id = child_records[0]["trace_id"]
  493. await _update_goal_start(
  494. store, trace_id, goal_id,
  495. "delegate" if single else "explore",
  496. all_sub_trace_ids,
  497. )
  498. goal_started = True
  499. # 创建执行协程。Trace 在上面已经按批次预创建,
  500. # 因此孩子配额不会被并发调用绕过。
  501. coros = []
  502. for i, (task_item, msgs, child_record) in enumerate(
  503. zip(tasks, per_agent_msgs, child_records)
  504. ):
  505. cur_stid = child_record["trace_id"]
  506. child_depth = child_record["depth"]
  507. if child_record["is_new"]:
  508. await broadcast_sub_trace_started(
  509. trace_id, cur_stid, goal_id or "",
  510. child_record["agent_type"], task_item,
  511. )
  512. # 注册为活跃协作者
  513. collab_name = task_item[:30] if single and not continued else (
  514. f"delegate-{cur_stid[:8]}" if single else f"explore-{i+1}"
  515. )
  516. await _update_collaborator(
  517. store, trace_id,
  518. name=collab_name, sub_trace_id=cur_stid,
  519. status="running", summary=task_item[:80],
  520. )
  521. # 构建消息
  522. agent_msgs = list(msgs) + [{"role": "user", "content": task_item}]
  523. allowed_tools = _get_allowed_tools(context, child_depth)
  524. debug = getattr(runner, 'debug', False)
  525. agent_label = (agent_type or ("delegate" if single else f"explore-{i+1}"))
  526. debug_printer = _make_event_printer(agent_label) if debug else None
  527. # allowed_tools 已是当前深度的精确白名单。
  528. on_event = _make_interactive_handler(
  529. runner, cur_stid, trace_id, debug_printer=debug_printer
  530. )
  531. coro = runner.run_result(
  532. messages=agent_msgs,
  533. config=_make_run_config(
  534. trace_id=cur_stid,
  535. agent_type=agent_type or ("delegate" if single else "explore"),
  536. max_iterations=50 if single else 50,
  537. model=parent_trace.model if parent_trace else "gpt-4o",
  538. uid=parent_trace.uid if parent_trace else None,
  539. tools=allowed_tools,
  540. tool_groups=[], # tools 是精确白名单,不再合并默认 core 组
  541. name=task_item[:50],
  542. skills=skills,
  543. knowledge=context.get("knowledge_config"),
  544. context=child_record["context"],
  545. ),
  546. on_event=on_event,
  547. )
  548. coros.append((i, cur_stid, collab_name, coro))
  549. # continue_from 不进入新建临界区,但仍需要恢复 Goal 的运行状态。
  550. if not goal_started:
  551. await _update_goal_start(
  552. store, trace_id, goal_id,
  553. "delegate" if single else "explore",
  554. all_sub_trace_ids,
  555. )
  556. # 执行
  557. if single:
  558. # 单任务直接执行(带异常处理)
  559. _, stid, collab_name, coro = coros[0]
  560. try:
  561. result = await coro
  562. await broadcast_sub_trace_completed(
  563. trace_id, stid,
  564. result.get("status", "completed"),
  565. result.get("summary", ""),
  566. result.get("stats", {}),
  567. )
  568. await _update_collaborator(
  569. store, trace_id,
  570. name=collab_name, sub_trace_id=stid,
  571. status=result.get("status", "completed"),
  572. summary=result.get("summary", "")[:80],
  573. )
  574. formatted = _format_single_result(result, stid, continued)
  575. await _update_goal_complete(
  576. store, trace_id, goal_id,
  577. result.get("status", "completed"),
  578. formatted["summary"],
  579. )
  580. return formatted
  581. except Exception as e:
  582. error_msg = str(e)
  583. await broadcast_sub_trace_completed(
  584. trace_id, stid, "failed", error_msg, {},
  585. )
  586. await _update_collaborator(
  587. store, trace_id,
  588. name=collab_name, sub_trace_id=stid,
  589. status="failed", summary=error_msg[:80],
  590. )
  591. await _update_goal_complete(
  592. store, trace_id, goal_id,
  593. "failed", f"委托任务失败: {error_msg}",
  594. )
  595. return {
  596. "mode": "delegate",
  597. "status": "failed",
  598. "error": error_msg,
  599. "sub_trace_id": stid,
  600. }
  601. else:
  602. # 检查父 Agent 是否开启了并发配置
  603. is_parallel = getattr(runner.config, "parallel_tool_execution", True) if runner and hasattr(runner, "config") else True
  604. if is_parallel:
  605. # 多任务并行执行
  606. raw_results = await asyncio.gather(
  607. *(coro for _, _, _, coro in coros),
  608. return_exceptions=True,
  609. )
  610. else:
  611. # 多任务串行执行(为了省显存/内存)
  612. raw_results = []
  613. for _, _, _, coro in coros:
  614. try:
  615. res = await coro
  616. raw_results.append(res)
  617. except Exception as e:
  618. raw_results.append(e)
  619. processed_results = []
  620. for idx, raw in enumerate(raw_results):
  621. _, stid, collab_name, _ = coros[idx]
  622. if isinstance(raw, Exception):
  623. error_result = {
  624. "status": "failed",
  625. "summary": f"执行出错: {str(raw)}",
  626. "stats": {"total_messages": 0, "total_tokens": 0, "total_cost": 0.0},
  627. }
  628. processed_results.append(error_result)
  629. await broadcast_sub_trace_completed(
  630. trace_id, stid, "failed", str(raw), {},
  631. )
  632. await _update_collaborator(
  633. store, trace_id,
  634. name=collab_name, sub_trace_id=stid,
  635. status="failed", summary=str(raw)[:80],
  636. )
  637. else:
  638. processed_results.append(raw)
  639. await broadcast_sub_trace_completed(
  640. trace_id, stid,
  641. raw.get("status", "completed"),
  642. raw.get("summary", ""),
  643. raw.get("stats", {}),
  644. )
  645. await _update_collaborator(
  646. store, trace_id,
  647. name=collab_name, sub_trace_id=stid,
  648. status=raw.get("status", "completed"),
  649. summary=raw.get("summary", "")[:80],
  650. )
  651. formatted = _format_multi_result(tasks, processed_results, all_sub_trace_ids)
  652. await _update_goal_complete(
  653. store, trace_id, goal_id,
  654. formatted["status"],
  655. formatted["summary"],
  656. )
  657. return formatted
  658. # ===== 远端 Agent 路由 =====
  659. async def _run_remote_agent(
  660. agent_type: str,
  661. task: str,
  662. messages: Optional[Messages],
  663. continue_from: Optional[str],
  664. skills: Optional[List[str]] = None,
  665. ) -> Dict[str, Any]:
  666. """
  667. 通过 HTTP 调用 KnowHub 服务器上的远端 Agent。
  668. 远端 Agent 的 tools / model / prompt 由服务器端 preset 决定。
  669. skills 由 caller 指定,服务器按 agent_type 的白名单过滤。
  670. """
  671. import httpx
  672. payload = {
  673. "agent_type": agent_type,
  674. "task": task,
  675. "messages": messages,
  676. "continue_from": continue_from,
  677. "skills": skills,
  678. }
  679. api_base = _knowhub_api()
  680. timeout = _remote_agent_timeout()
  681. try:
  682. async with httpx.AsyncClient(timeout=timeout) as client:
  683. response = await client.post(f"{api_base}/api/agent", json=payload)
  684. response.raise_for_status()
  685. result = response.json()
  686. return {
  687. "mode": "remote",
  688. "agent_type": agent_type,
  689. "sub_trace_id": result.get("sub_trace_id"),
  690. "status": result.get("status", "completed"),
  691. "summary": result.get("summary", ""),
  692. "stats": result.get("stats", {}),
  693. "error": result.get("error"),
  694. }
  695. except httpx.HTTPStatusError as e:
  696. return {
  697. "mode": "remote",
  698. "agent_type": agent_type,
  699. "status": "failed",
  700. "error": f"HTTP {e.response.status_code}: {e.response.text[:200]}",
  701. }
  702. except Exception as e:
  703. return {
  704. "mode": "remote",
  705. "agent_type": agent_type,
  706. "status": "failed",
  707. "error": f"远端调用失败: {type(e).__name__}: {e}",
  708. }
  709. # ===== 工具定义 =====
  710. @tool(description="创建子 Agent 执行任务(本地执行或路由到远端服务器,由 agent_type 决定)", hidden_params=["context"], groups=["core"])
  711. async def agent(
  712. task: Union[str, List[str]],
  713. messages: Optional[Union[Messages, List[Messages]]] = None,
  714. continue_from: Optional[str] = None,
  715. agent_type: Optional[str] = None,
  716. skills: Optional[List[str]] = None,
  717. context: Optional[dict] = None,
  718. ) -> Dict[str, Any]:
  719. """
  720. 创建子 Agent 执行任务。
  721. 路由规则:
  722. - agent_type 以 "remote_" 开头:HTTP 调用 KnowHub 服务器的 /api/agent(仅单任务,无本地文件访问)
  723. - 否则本地执行:单任务 str = delegate(全量工具);多任务 List[str] = explore(并行、只读)
  724. Args:
  725. task: 任务描述。字符串=单任务,列表=多任务并行(远端模式只支持单任务)
  726. messages: 预置消息。1D 列表=所有 agent 共享;2D 列表=per-agent
  727. continue_from: 继续已有 trace(仅单任务)
  728. agent_type: 子 Agent 类型。带 "remote_" 前缀走远端;否则本地 preset
  729. skills: 指定本次调用使用的 skill 列表
  730. - 本地:附加到 system prompt
  731. - 远端:由服务器按 agent_type 白名单过滤(如 remote_librarian 允许 ask_strategy / upload_strategy)
  732. context: 框架自动注入的上下文
  733. """
  734. # 远端路由:agent_type 以 remote_ 开头
  735. if agent_type and agent_type.startswith(REMOTE_PREFIX):
  736. if not isinstance(task, str):
  737. return {"status": "failed", "error": "remote agent 只支持单任务 (task: str)"}
  738. # 归一化 messages:远端只接受 1D Messages 或 None
  739. remote_msgs: Optional[Messages] = None
  740. if messages is not None:
  741. if messages and isinstance(messages[0], list):
  742. return {"status": "failed", "error": "remote agent 不支持 2D messages (per-agent)"}
  743. remote_msgs = messages
  744. return await _run_remote_agent(
  745. agent_type=agent_type,
  746. task=task,
  747. messages=remote_msgs,
  748. continue_from=continue_from,
  749. skills=skills,
  750. )
  751. # 本地路径:需要 context
  752. if not context:
  753. return {"status": "failed", "error": "context is required"}
  754. store = context.get("store")
  755. trace_id = context.get("trace_id")
  756. goal_id = context.get("goal_id")
  757. runner = context.get("runner")
  758. missing = []
  759. if not store:
  760. missing.append("store")
  761. if not trace_id:
  762. missing.append("trace_id")
  763. if not runner:
  764. missing.append("runner")
  765. if missing:
  766. return {"status": "failed", "error": f"Missing required context: {', '.join(missing)}"}
  767. # 归一化 task → list
  768. if isinstance(task, str):
  769. task_str = task.strip()
  770. if task_str.startswith("[") and task_str.endswith("]"):
  771. try:
  772. import json
  773. parsed_task = json.loads(task_str)
  774. if isinstance(parsed_task, list):
  775. task = parsed_task
  776. except:
  777. pass
  778. single = isinstance(task, str)
  779. tasks = [task] if single else task
  780. if not tasks:
  781. return {"status": "failed", "error": "task is required"}
  782. # 归一化 messages → List[Messages](per-agent)
  783. if messages is None:
  784. per_agent_msgs: List[Messages] = [[] for _ in tasks]
  785. elif messages and isinstance(messages[0], list):
  786. if len(messages) != len(tasks):
  787. return {
  788. "status": "failed",
  789. "error": (
  790. "2D messages must contain exactly one message list per task: "
  791. f"tasks={len(tasks)}, messages={len(messages)}"
  792. ),
  793. }
  794. per_agent_msgs = messages # 2D: per-agent
  795. else:
  796. per_agent_msgs = [messages] * len(tasks) # 1D: 共享
  797. if continue_from and not single:
  798. return {"status": "failed", "error": "continue_from requires single task"}
  799. return await _run_agents(
  800. tasks, per_agent_msgs, continue_from,
  801. store, trace_id, goal_id, runner, context,
  802. agent_type=agent_type,
  803. skills=skills,
  804. )
  805. @tool(description="评估目标执行结果是否满足要求", hidden_params=["context"], groups=["core"])
  806. async def evaluate(
  807. messages: Optional[Messages] = None,
  808. target_goal_id: Optional[str] = None,
  809. continue_from: Optional[str] = None,
  810. context: Optional[dict] = None,
  811. ) -> Dict[str, Any]:
  812. """
  813. 评估目标执行结果是否满足要求。
  814. 代码自动从 GoalTree 注入目标描述。模型把执行结果和上下文放在 messages 中。
  815. Args:
  816. messages: 执行结果和上下文消息(OpenAI 格式)
  817. target_goal_id: 要评估的目标 ID(默认当前 goal_id)
  818. continue_from: 继续已有评估 trace
  819. context: 框架自动注入的上下文
  820. """
  821. if not context:
  822. return {"status": "failed", "error": "context is required"}
  823. store = context.get("store")
  824. trace_id = context.get("trace_id")
  825. current_goal_id = context.get("goal_id")
  826. runner = context.get("runner")
  827. missing = []
  828. if not store:
  829. missing.append("store")
  830. if not trace_id:
  831. missing.append("trace_id")
  832. if not runner:
  833. missing.append("runner")
  834. if missing:
  835. return {"status": "failed", "error": f"Missing required context: {', '.join(missing)}"}
  836. # target_goal_id 默认 context["goal_id"]
  837. goal_id = target_goal_id or current_goal_id
  838. # 从 GoalTree 获取目标描述
  839. goal_desc = await _get_goal_description(store, trace_id, goal_id)
  840. # 构建 evaluator prompt
  841. eval_prompt = _build_evaluate_prompt(goal_desc, messages)
  842. # 获取父 Trace 信息
  843. parent_trace = await store.get_trace(trace_id)
  844. # 处理 continue_from 或创建新 Sub-Trace
  845. if continue_from:
  846. existing_trace = await store.get_trace(continue_from)
  847. if not existing_trace:
  848. return {"status": "failed", "error": f"Continue-from trace not found: {continue_from}"}
  849. sub_trace_id = continue_from
  850. goal_tree = await store.get_goal_tree(continue_from)
  851. mission = goal_tree.mission if goal_tree else eval_prompt
  852. sub_trace_ids = [{"trace_id": sub_trace_id, "mission": mission}]
  853. else:
  854. sub_trace_id = generate_sub_trace_id(trace_id, "evaluate")
  855. sub_trace = Trace(
  856. trace_id=sub_trace_id,
  857. mode="agent",
  858. task=eval_prompt,
  859. parent_trace_id=trace_id,
  860. parent_goal_id=current_goal_id,
  861. agent_type="evaluate",
  862. uid=parent_trace.uid if parent_trace else None,
  863. model=parent_trace.model if parent_trace else None,
  864. status="running",
  865. context={"created_by_tool": "evaluate"},
  866. created_at=datetime.now(),
  867. )
  868. await store.create_trace(sub_trace)
  869. await store.update_goal_tree(sub_trace_id, GoalTree(mission=eval_prompt))
  870. sub_trace_ids = [{"trace_id": sub_trace_id, "mission": eval_prompt}]
  871. # 广播 sub_trace_started
  872. await broadcast_sub_trace_started(
  873. trace_id, sub_trace_id, current_goal_id or "",
  874. "evaluate", eval_prompt,
  875. )
  876. # 更新主 Goal 为 in_progress
  877. await _update_goal_start(store, trace_id, current_goal_id, "evaluate", sub_trace_ids)
  878. # 注册为活跃协作者
  879. eval_name = f"评估: {(goal_id or 'unknown')[:20]}"
  880. await _update_collaborator(
  881. store, trace_id,
  882. name=eval_name, sub_trace_id=sub_trace_id,
  883. status="running", summary=f"评估 Goal {goal_id}",
  884. )
  885. # 执行评估
  886. try:
  887. # evaluate 使用只读工具 + goal
  888. allowed_tools = ["read_file", "grep_content", "glob_files", "goal"]
  889. result = await runner.run_result(
  890. messages=[{"role": "user", "content": eval_prompt}],
  891. config=_make_run_config(
  892. trace_id=sub_trace_id,
  893. agent_type="evaluate",
  894. model=parent_trace.model if parent_trace else "gpt-4o",
  895. uid=parent_trace.uid if parent_trace else None,
  896. tools=allowed_tools,
  897. tool_groups=[],
  898. exclude_tools=["agent", "evaluate", "bash_command"],
  899. name=f"评估: {goal_id}",
  900. ),
  901. on_event=_make_interactive_handler(
  902. runner, sub_trace_id, trace_id,
  903. debug_printer=_make_event_printer("evaluate") if getattr(runner, 'debug', False) else None,
  904. ),
  905. )
  906. await broadcast_sub_trace_completed(
  907. trace_id, sub_trace_id,
  908. result.get("status", "completed"),
  909. result.get("summary", ""),
  910. result.get("stats", {}),
  911. )
  912. await _update_collaborator(
  913. store, trace_id,
  914. name=eval_name, sub_trace_id=sub_trace_id,
  915. status=result.get("status", "completed"),
  916. summary=result.get("summary", "")[:80],
  917. )
  918. formatted_summary = result.get("summary", "")
  919. await _update_goal_complete(
  920. store, trace_id, current_goal_id,
  921. result.get("status", "completed"),
  922. formatted_summary,
  923. )
  924. return {
  925. "mode": "evaluate",
  926. "sub_trace_id": sub_trace_id,
  927. "continue_from": bool(continue_from),
  928. **result,
  929. "summary": formatted_summary,
  930. }
  931. except Exception as e:
  932. error_msg = str(e)
  933. await broadcast_sub_trace_completed(
  934. trace_id, sub_trace_id, "failed", error_msg, {},
  935. )
  936. await _update_collaborator(
  937. store, trace_id,
  938. name=eval_name, sub_trace_id=sub_trace_id,
  939. status="failed", summary=error_msg[:80],
  940. )
  941. await _update_goal_complete(
  942. store, trace_id, current_goal_id,
  943. "failed", f"评估任务失败: {error_msg}",
  944. )
  945. return {
  946. "mode": "evaluate",
  947. "status": "failed",
  948. "error": error_msg,
  949. "sub_trace_id": sub_trace_id,
  950. }