subagent.py 32 KB

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