| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643 |
- """
- Sub-Agent 工具 - agent / evaluate
- agent: 创建子 Agent 执行任务。
- - 本地:`agent_type` 无 `remote_` 前缀,进程内执行(单任务 delegate / 多任务并行 explore)
- - 远端:`agent_type` 以 `remote_` 开头,HTTP 路由到 KnowHub 服务器的 /api/agent
- evaluate: 评估目标执行结果是否满足要求
- """
- import asyncio
- import json
- import os
- from datetime import datetime
- from typing import Any, Dict, List, Optional, Union
- from pydantic import ValidationError
- from cyber_agent.tools import tool
- from cyber_agent.core.agent_mode import (
- AgentMode,
- AgentPolicy,
- RECURSIVE_CAPABILITY_TOOLS_CONTEXT_KEY,
- RECURSIVE_CHILD_EXECUTION_MODE_CONTEXT_KEY,
- RECURSIVE_MAX_PARALLEL_CHILDREN_CONTEXT_KEY,
- apply_policy_to_context,
- assert_removed_config_absent,
- policy_from_context,
- validate_recursive_child_execution,
- )
- from cyber_agent.core.task_protocol import (
- TaskBrief,
- TaskReport,
- ensure_task_protocol,
- format_task_brief,
- pending_review_entry,
- protocol_error_report,
- stopped_task_report,
- )
- from cyber_agent.trace.models import Trace, Messages
- from cyber_agent.trace.trace_id import generate_sub_trace_id
- from cyber_agent.trace.goal_models import GoalTree
- from cyber_agent.trace.websocket import broadcast_sub_trace_started, broadcast_sub_trace_completed
- # ===== 远端路由常量 =====
- REMOTE_PREFIX = "remote_"
- # POC 阶段使用进程内锁保护“检查配额 -> 创建 Trace”。
- # 多 worker 部署时需要换成跨进程的原子配额。
- _LOCAL_AGENT_SPAWN_LOCK = asyncio.Lock()
- def _knowhub_api() -> str:
- """运行时读取 KNOWHUB_API,避免 module-load 时 .env 尚未加载的情况"""
- return os.getenv("KNOWHUB_API", "http://localhost:9999").rstrip("/")
- def _remote_agent_timeout() -> float:
- return float(os.getenv("REMOTE_AGENT_TIMEOUT", "600"))
- # 兼容旧代码对 module-level 常量的引用(运行时值 = 首次 import 时的快照)
- KNOWHUB_API = _knowhub_api()
- REMOTE_AGENT_TIMEOUT = _remote_agent_timeout()
- # ===== prompts =====
- # ===== 评估任务 =====
- EVALUATE_PROMPT_TEMPLATE = """# 评估任务
- 请评估以下任务的执行结果是否满足要求。
- ## 目标描述
- {goal_description}
- ## 执行结果
- {result_text}
- ## 输出格式
- ## 评估结论
- [通过/不通过]
- ## 评估理由
- [详细说明通过或不通过原因]
- ## 修改建议(如果不通过)
- 1. [建议1]
- 2. [建议2]
- """
- # ===== 结果格式化 =====
- DELEGATE_RESULT_HEADER = "## 委托任务完成\n"
- DELEGATE_SAVED_KNOWLEDGE_HEADER = "**保存的知识** ({count} 条):"
- DELEGATE_STATS_HEADER = "**执行统计**:"
- EXPLORE_RESULT_HEADER = "## 探索结果\n"
- EXPLORE_BRANCH_TEMPLATE = "### 方案 {branch_name}: {task}"
- EXPLORE_STATUS_SUCCESS = "**状态**: ✓ 完成"
- EXPLORE_STATUS_FAILED = "**状态**: ✗ 失败"
- EXPLORE_STATUS_ERROR = "**状态**: ✗ 异常"
- EXPLORE_SUMMARY_HEADER = "## 总结"
- def build_evaluate_prompt(goal_description: str, result_text: str) -> str:
- return EVALUATE_PROMPT_TEMPLATE.format(
- goal_description=goal_description,
- result_text=result_text or "(无执行结果)",
- )
- def _make_run_config(**kwargs):
- """延迟导入 RunConfig 以避免循环导入"""
- from cyber_agent.core.runner import RunConfig
- return RunConfig(**kwargs)
- # ===== 辅助函数 =====
- async def _update_collaborator(
- store, trace_id: str,
- name: str, sub_trace_id: str,
- status: str, summary: str = "",
- ) -> None:
- """
- 更新 trace.context["collaborators"] 中的协作者信息。
- 如果同名协作者已存在则更新,否则追加。
- """
- trace = await store.get_trace(trace_id)
- if not trace:
- return
- collaborators = trace.context.get("collaborators", [])
- # 查找已有记录
- existing = None
- for c in collaborators:
- if c.get("trace_id") == sub_trace_id:
- existing = c
- break
- if existing:
- existing["status"] = status
- if summary:
- existing["summary"] = summary
- else:
- collaborators.append({
- "name": name,
- "type": "agent",
- "trace_id": sub_trace_id,
- "status": status,
- "summary": summary,
- })
- trace.context["collaborators"] = collaborators
- await store.update_trace(trace_id, context=trace.context)
- async def _update_goal_start(
- store, trace_id: str, goal_id: str, mode: str,
- sub_trace_ids: List[Dict[str, str]],
- *,
- accumulate_sub_trace_ids: bool,
- status: str = "in_progress",
- ) -> None:
- """标记 Goal 开始执行"""
- if not goal_id:
- return
- next_entries = sub_trace_ids
- if accumulate_sub_trace_ids:
- tree = await store.get_goal_tree(trace_id)
- goal = tree.find(goal_id) if tree else None
- existing_entries = goal.sub_trace_ids if goal and goal.sub_trace_ids else []
- merged_entries: Dict[str, Dict[str, str]] = {}
- for entry in [*existing_entries, *sub_trace_ids]:
- if isinstance(entry, str):
- merged_entries[entry] = {"trace_id": entry, "mission": entry}
- elif isinstance(entry, dict) and entry.get("trace_id"):
- merged_entries[entry["trace_id"]] = entry
- next_entries = list(merged_entries.values())
- await store.update_goal(
- trace_id, goal_id,
- type="agent_call",
- agent_call_mode=mode,
- status=status,
- sub_trace_ids=next_entries,
- )
- async def _update_goal_complete(
- store, trace_id: str, goal_id: str,
- status: str, summary: str,
- ) -> None:
- """标记 Goal 完成"""
- if not goal_id:
- return
- await store.update_goal(
- trace_id, goal_id,
- status=status,
- summary=summary,
- )
- async def _load_or_create_task_report(
- store,
- child_trace_id: str,
- fallback_reason: str,
- child_result_status: str,
- generated_at_sequence: int,
- ) -> TaskReport:
- child = await store.get_trace(child_trace_id)
- if not child:
- return protocol_error_report(child_trace_id, fallback_reason)
- state = ensure_task_protocol(child.context)
- report_data = state.get("task_report")
- execution_stopped = (
- child_result_status == "stopped"
- or child.status == "stopped"
- )
- execution_failed = (
- child_result_status != "completed"
- or child.status != "completed"
- )
- if report_data:
- try:
- report = TaskReport.model_validate(report_data)
- if report.child_trace_id != child_trace_id:
- if report.model_dump() not in state["report_history"]:
- state["report_history"].append(report.model_dump())
- fallback_reason = (
- "Child persisted a TaskReport whose child_trace_id does not "
- "match its Trace"
- )
- elif execution_stopped:
- if report.outcome in {"failed", "protocol_error"}:
- return report
- if report.model_dump() not in state["report_history"]:
- state["report_history"].append(report.model_dump())
- fallback_reason = "Child Agent execution was stopped"
- elif not execution_failed or report.outcome in {"failed", "protocol_error"}:
- return report
- else:
- if report.model_dump() not in state["report_history"]:
- state["report_history"].append(report.model_dump())
- fallback_reason = (
- f"Child execution ended as {child_result_status} after submitting "
- f"a {report.outcome} TaskReport: {fallback_reason}"
- )
- except ValidationError:
- fallback_reason = "Child persisted an invalid TaskReport"
- report = (
- stopped_task_report(child_trace_id, fallback_reason)
- if execution_stopped
- else protocol_error_report(child_trace_id, fallback_reason)
- )
- state["task_report"] = report.model_dump()
- state["task_report_submitted_at_sequence"] = generated_at_sequence
- await store.update_trace(child_trace_id, context=child.context)
- return report
- async def _record_pending_task_reports(
- store,
- parent_trace: Trace,
- goal_id: str | None,
- child_results: List[tuple[str, Dict[str, Any]]],
- received_at_sequence: int,
- ) -> List[TaskReport]:
- state = ensure_task_protocol(parent_trace.context)
- reports: List[TaskReport] = []
- for child_trace_id, result in child_results:
- reason = result.get("error") or result.get("summary") or "Child ended without TaskReport"
- report = await _load_or_create_task_report(
- store,
- child_trace_id,
- reason,
- result.get("status", "unknown"),
- received_at_sequence,
- )
- state["pending_reviews"][child_trace_id] = pending_review_entry(
- goal_id=goal_id,
- report=report,
- received_at_sequence=received_at_sequence,
- )
- reports.append(report)
- await store.update_trace(parent_trace.trace_id, context=parent_trace.context)
- if goal_id:
- await store.update_goal(
- parent_trace.trace_id,
- goal_id,
- cascade_completion=False,
- status="pending_review",
- )
- return reports
- def _project_goal_status(context: dict, goal_id: str | None, status: str) -> None:
- tree = context.get("goal_tree")
- goal = tree.find(goal_id) if tree and goal_id else None
- if goal:
- goal.status = status
- def _aggregate_stats(results: List[Dict[str, Any]]) -> Dict[str, Any]:
- """聚合多个结果的统计信息"""
- total_messages = 0
- total_tokens = 0
- total_cost = 0.0
- for result in results:
- if isinstance(result, dict) and "stats" in result:
- stats = result["stats"]
- total_messages += stats.get("total_messages", 0)
- total_tokens += stats.get("total_tokens", 0)
- total_cost += stats.get("total_cost", 0.0)
- return {
- "total_messages": total_messages,
- "total_tokens": total_tokens,
- "total_cost": total_cost
- }
- async def _stopped_child_result(store, runner, trace_id: str) -> Dict[str, Any]:
- """停止尚未启动的预创建孩子,不产生任何模型调用或消息统计。"""
- await store.update_trace(
- trace_id,
- status="stopped",
- completed_at=datetime.now(),
- )
- event = getattr(runner, "_cancel_events", {}).get(trace_id)
- release_trace = getattr(runner, "release_recursive_trace", None)
- if release_trace:
- release_trace(trace_id, event)
- return {
- "status": "stopped",
- "summary": "Agent execution stopped.",
- "error": "Child Agent execution was stopped",
- "saved_knowledge_ids": [],
- "stats": {"total_messages": 0, "total_tokens": 0, "total_cost": 0.0},
- }
- async def _execute_child_spec(
- spec: Dict[str, Any],
- runner,
- store,
- semaphore: Optional[asyncio.Semaphore] = None,
- ) -> Dict[str, Any]:
- """延迟启动一个孩子;排队期间收到停止信号时不进入 Runner。"""
- trace_id = spec["trace_id"]
- async def execute() -> Dict[str, Any]:
- is_cancelled = getattr(runner, "is_cancel_requested", lambda _tid: False)
- if spec["recursive"] and is_cancelled(trace_id):
- return await _stopped_child_result(store, runner, trace_id)
- return await runner.run_result(
- messages=spec["messages"],
- config=spec["config"],
- on_event=spec["on_event"],
- )
- if semaphore is None:
- return await execute()
- is_cancelled = getattr(runner, "is_cancel_requested", lambda _tid: False)
- if spec["recursive"] and is_cancelled(trace_id):
- return await _stopped_child_result(store, runner, trace_id)
- async with semaphore:
- return await execute()
- def _get_recursive_parent_capabilities(context: dict) -> Optional[set[str]]:
- runner = context.get("runner")
- capabilities = context.get(RECURSIVE_CAPABILITY_TOOLS_CONTEXT_KEY)
- if (
- not runner
- or not hasattr(runner, "tools")
- or not hasattr(runner.tools, "get_tool_names")
- or not isinstance(capabilities, list)
- or any(not isinstance(name, str) for name in capabilities)
- ):
- return None
- return set(runner.tools.get_tool_names()) & set(capabilities)
- def _get_allowed_tools(
- context: dict,
- agent_depth: int,
- policy: AgentPolicy,
- ) -> Optional[List[str]]:
- """按子 Agent 深度生成唯一的工具权限列表。"""
- runner = context.get("runner")
- if runner and hasattr(runner, "tools") and hasattr(runner.tools, "get_tool_names"):
- registered_tools = set(runner.tools.get_tool_names())
- if policy.mode is AgentMode.RECURSIVE:
- allowed_tools = _get_recursive_parent_capabilities(context)
- if allowed_tools is None:
- return None
- else:
- # Legacy 保留原行为:从全局 Registry 取工具全集。
- allowed_tools = registered_tools
- blocked_tools = {"evaluate", "bash_command"}
- if not policy.requires_task_protocol:
- blocked_tools.update({"submit_task_report", "review_task_result"})
- if policy.mode is AgentMode.LEGACY or agent_depth >= policy.max_depth:
- blocked_tools.add("agent")
- return sorted(allowed_tools - blocked_tools)
- return None
- async def _resolve_trace_lineage(store, trace: Trace) -> tuple[int, str]:
- """返回 Trace 的 (agent_depth, root_trace_id)。
- 新 Trace 直接读 context;旧 Trace 缺少字段时沿 parent_trace_id
- 回溯,不依赖 Trace ID 字符串格式。
- """
- depth = trace.context.get("agent_depth")
- root_trace_id = trace.context.get("root_trace_id")
- if isinstance(depth, int) and depth >= 0 and isinstance(root_trace_id, str):
- return depth, root_trace_id
- depth = 0
- current = trace
- root_trace_id = trace.trace_id
- visited = {trace.trace_id}
- while current.parent_trace_id:
- parent_id = current.parent_trace_id
- if parent_id in visited:
- break
- visited.add(parent_id)
- parent = await store.get_trace(parent_id)
- if not parent:
- break
- depth += 1
- root_trace_id = parent.trace_id
- current = parent
- return depth, root_trace_id
- def _format_single_result(result: Dict[str, Any], sub_trace_id: str, continued: bool) -> Dict[str, Any]:
- """格式化单任务(delegate)结果"""
- lines = [DELEGATE_RESULT_HEADER]
- summary = result.get("summary", "")
- if summary:
- lines.append(summary)
- lines.append("")
- # 添加保存的知识 ID
- saved_knowledge_ids = result.get("saved_knowledge_ids", [])
- if saved_knowledge_ids:
- lines.append("---\n")
- lines.append(DELEGATE_SAVED_KNOWLEDGE_HEADER.format(count=len(saved_knowledge_ids)))
- for kid in saved_knowledge_ids:
- lines.append(f"- {kid}")
- lines.append("")
- lines.append("---\n")
- lines.append(DELEGATE_STATS_HEADER)
- stats = result.get("stats", {})
- if stats:
- lines.append(f"- 消息数: {stats.get('total_messages', 0)}")
- lines.append(f"- Tokens: {stats.get('total_tokens', 0)}")
- lines.append(f"- 成本: ${stats.get('total_cost', 0.0):.4f}")
- formatted_summary = "\n".join(lines)
- return {
- "mode": "delegate",
- "sub_trace_id": sub_trace_id,
- "continue_from": continued,
- "saved_knowledge_ids": saved_knowledge_ids, # 传递给父 agent
- **result,
- "summary": formatted_summary,
- }
- def _format_multi_result(
- tasks: List[str], results: List[Dict[str, Any]], sub_trace_ids: List[Dict]
- ) -> Dict[str, Any]:
- """格式化多任务(explore)聚合结果"""
- lines = [EXPLORE_RESULT_HEADER]
- successful = 0
- failed = 0
- total_tokens = 0
- total_cost = 0.0
- for i, (task_item, result) in enumerate(zip(tasks, results)):
- branch_name = chr(ord('A') + i)
- lines.append(EXPLORE_BRANCH_TEMPLATE.format(branch_name=branch_name, task=task_item))
- if isinstance(result, dict):
- status = result.get("status", "unknown")
- if status == "completed":
- lines.append(EXPLORE_STATUS_SUCCESS)
- successful += 1
- else:
- lines.append(EXPLORE_STATUS_FAILED)
- failed += 1
- summary = result.get("summary", "")
- if summary:
- lines.append(f"**摘要**: {summary[:200]}...")
- stats = result.get("stats", {})
- if stats:
- messages = stats.get("total_messages", 0)
- tokens = stats.get("total_tokens", 0)
- cost = stats.get("total_cost", 0.0)
- lines.append(f"**统计**: {messages} messages, {tokens} tokens, ${cost:.4f}")
- total_tokens += tokens
- total_cost += cost
- else:
- lines.append(EXPLORE_STATUS_ERROR)
- failed += 1
- lines.append("")
- lines.append("---\n")
- lines.append(EXPLORE_SUMMARY_HEADER)
- lines.append(f"- 总分支数: {len(tasks)}")
- lines.append(f"- 成功: {successful}")
- lines.append(f"- 失败: {failed}")
- lines.append(f"- 总 tokens: {total_tokens}")
- lines.append(f"- 总成本: ${total_cost:.4f}")
- aggregated_summary = "\n".join(lines)
- overall_status = "completed" if successful > 0 else "failed"
- return {
- "mode": "explore",
- "status": overall_status,
- "summary": aggregated_summary,
- "sub_trace_ids": sub_trace_ids,
- "tasks": tasks,
- "stats": _aggregate_stats(results),
- }
- async def _get_goal_description(store, trace_id: str, goal_id: str) -> str:
- """从 GoalTree 获取目标描述"""
- if not goal_id:
- return ""
- goal_tree = await store.get_goal_tree(trace_id)
- if goal_tree:
- target_goal = goal_tree.find(goal_id)
- if target_goal:
- return target_goal.description
- return f"Goal {goal_id}"
- def _build_evaluate_prompt(goal_description: str, messages: Optional[Messages]) -> str:
- """
- 构建评估 prompt。
- Args:
- goal_description: 代码从 GoalTree 注入的目标描述
- messages: 模型提供的消息(执行结果+上下文)
- """
- # 从 messages 提取文本内容
- result_text = ""
- if messages:
- parts = []
- for msg in messages:
- content = msg.get("content", "")
- if isinstance(content, str):
- parts.append(content)
- elif isinstance(content, list):
- # 多模态内容,提取文本部分
- for item in content:
- if isinstance(item, dict) and item.get("type") == "text":
- parts.append(item.get("text", ""))
- result_text = "\n".join(parts)
- return build_evaluate_prompt(goal_description, result_text)
- def _make_event_printer(label: str):
- """
- 创建子 Agent 执行过程打印函数。
- 当父 runner.debug=True 时,传给 run_result(on_event=...),
- 实时输出子 Agent 的工具调用和助手消息。
- """
- prefix = f" [{label}]"
- def on_event(item):
- from cyber_agent.trace.models import Trace, Message
- if isinstance(item, Message):
- if item.role == "assistant":
- content = item.content
- if isinstance(content, dict):
- text = content.get("text", "")
- tool_calls = content.get("tool_calls")
- if text:
- preview = text[:120] + "..." if len(text) > 120 else text
- print(f"{prefix} {preview}")
- if tool_calls:
- for tc in tool_calls:
- name = tc.get("function", {}).get("name", "unknown")
- print(f"{prefix} 🛠️ {name}")
- elif item.role == "tool":
- content = item.content
- if isinstance(content, dict):
- name = content.get("tool_name", "unknown")
- desc = item.description or ""
- desc_short = (desc[:60] + "...") if len(desc) > 60 else desc
- suffix = f": {desc_short}" if desc_short else ""
- print(f"{prefix} ✅ {name}{suffix}")
- elif isinstance(item, Trace):
- if item.status == "completed":
- print(f"{prefix} ✓ 完成")
- elif item.status == "failed":
- err = (item.error_message or "")[:80]
- print(f"{prefix} ✗ 失败: {err}")
- return on_event
- def _make_interactive_handler(runner, sub_trace_id: str, parent_trace_id: str, debug_printer=None):
- """
- 创建支持 stdin 交互检查的 on_event 回调。
- 在每个子 Agent 事件触发时检查 stdin,检测到暂停/退出信号后
- 通过 cancel_event.set() 停止子 agent 和父 agent 的执行。
- """
- def on_event(item):
- # 先执行 debug 打印
- if debug_printer:
- debug_printer(item)
- # 检查 stdin
- check_fn = getattr(runner, 'stdin_check', None)
- if not check_fn:
- return
- cmd = check_fn()
- if cmd in ('pause', 'quit'):
- request_stop = getattr(runner, "request_stop", None)
- if request_stop:
- request_stop(parent_trace_id)
- else:
- for tid in (sub_trace_id, parent_trace_id):
- ev = runner._cancel_events.get(tid)
- if ev:
- ev.set()
- return on_event
- # ===== 统一内部执行函数 =====
- async def _run_agents(
- tasks: List[str],
- per_agent_msgs: List[Messages],
- continue_from: Optional[str],
- store, trace_id: str, goal_id: str, runner, context: dict,
- agent_type: Optional[str] = None,
- skills: Optional[List[str]] = None,
- task_briefs: Optional[List[TaskBrief]] = None,
- ) -> Dict[str, Any]:
- """
- 统一 agent 执行逻辑。
- single (len(tasks)==1): delegate 模式,全量工具
- multi (len(tasks)>1): explore 模式,并行执行
- 本地 Sub-Agent 的深度、直接孩子配额和工具权限由父 Trace
- 已持久化的 AgentPolicy 决定,不重新读取环境变量。
- """
- single = len(tasks) == 1
- parent_trace = await store.get_trace(trace_id)
- if not parent_trace:
- return {"status": "failed", "error": f"Parent trace not found: {trace_id}"}
- try:
- policy = policy_from_context(parent_trace.context)
- except ValueError as exc:
- return {"status": "failed", "error": str(exc)}
- if (
- policy.mode is AgentMode.RECURSIVE
- and _get_recursive_parent_capabilities(context) is None
- ):
- return {
- "status": "failed",
- "error": "Recursive agent capability context is missing or invalid",
- }
- child_execution_mode = "parallel"
- max_parallel_children = 2
- if policy.mode is AgentMode.RECURSIVE:
- try:
- child_execution_mode, max_parallel_children = (
- validate_recursive_child_execution(
- context.get(
- RECURSIVE_CHILD_EXECUTION_MODE_CONTEXT_KEY,
- "sequential",
- ),
- context.get(
- RECURSIVE_MAX_PARALLEL_CHILDREN_CONTEXT_KEY,
- 2,
- ),
- )
- )
- except ValueError as exc:
- return {"status": "failed", "error": str(exc)}
- protocol_state = None
- approved_action = None
- if policy.requires_task_protocol:
- protocol_state = ensure_task_protocol(parent_trace.context)
- if goal_id:
- parent_tree = await store.get_goal_tree(trace_id)
- parent_goal = parent_tree.find(goal_id) if parent_tree else None
- if parent_goal and parent_goal.status in {"completed", "failed", "abandoned"}:
- return {
- "status": "failed",
- "error": "Cannot create a child from a terminal Goal",
- }
- if protocol_state["pending_reviews"]:
- return {
- "status": "failed",
- "error": "Review pending child TaskReports before creating another child",
- }
- if not task_briefs or len(task_briefs) != len(tasks):
- return {"status": "failed", "error": "Recursive revision 2 requires task_brief"}
- if protocol_state["next_actions"]:
- approved_action = protocol_state["next_actions"][0]
- decision = approved_action["decision"]
- expected_brief = approved_action.get("task_brief")
- if decision == "REVISE_CHILD":
- if continue_from != approved_action["child_trace_id"] or len(tasks) != 1:
- return {
- "status": "failed",
- "error": "REVISE_CHILD requires continue_from for the reviewed child",
- }
- if expected_brief and task_briefs[0].model_dump() != expected_brief:
- return {
- "status": "failed",
- "error": "task_brief does not match the approved revision",
- }
- else:
- if continue_from or len(tasks) != 1:
- return {
- "status": "failed",
- "error": f"{decision} requires exactly one new approved child",
- }
- if not expected_brief or task_briefs[0].model_dump() != expected_brief:
- return {
- "status": "failed",
- "error": "task_brief does not match the approved next task",
- }
- elif continue_from:
- return {
- "status": "failed",
- "error": "continue_from requires a pending REVISE_CHILD action",
- }
- # continue_from: 复用已有 trace(仅 single)
- sub_trace_id = None
- continued = False
- goal_started = False
- child_records: List[Dict[str, Any]] = []
- all_sub_trace_ids: List[Dict[str, str]] = []
- if single and continue_from:
- existing = await store.get_trace(continue_from)
- if not existing:
- return {"status": "failed", "error": f"Continue-from trace not found: {continue_from}"}
- if existing.parent_trace_id != trace_id:
- return {
- "status": "failed",
- "error": "continue_from must reference a direct child of the current trace",
- }
- if existing.uid != parent_trace.uid:
- return {
- "status": "failed",
- "error": "continue_from trace owner does not match the current trace owner",
- }
- if existing.context.get("created_by_tool") != "agent":
- return {
- "status": "failed",
- "error": "continue_from must reference a child created by the agent tool",
- }
- try:
- child_policy = policy_from_context(existing.context)
- except ValueError as exc:
- return {"status": "failed", "error": str(exc)}
- if (
- child_policy.mode is not policy.mode
- or child_policy.revision != policy.revision
- ):
- return {
- "status": "failed",
- "error": "continue_from trace Agent mode does not match the current trace",
- }
- sub_trace_id = continue_from
- continued = True
- goal_tree = await store.get_goal_tree(continue_from)
- mission = goal_tree.mission if goal_tree else tasks[0]
- parent_depth, expected_root_trace_id = await _resolve_trace_lineage(
- store, parent_trace
- )
- child_depth, root_trace_id = await _resolve_trace_lineage(store, existing)
- if (
- child_depth != parent_depth + 1
- or root_trace_id != expected_root_trace_id
- ):
- return {
- "status": "failed",
- "error": "continue_from trace lineage does not match the current trace",
- }
- if child_depth > policy.max_depth:
- return {
- "status": "failed",
- "error": (
- "continue_from trace exceeds the persisted Agent mode depth: "
- f"depth={child_depth}, max={policy.max_depth}"
- ),
- }
- all_sub_trace_ids = [{"trace_id": sub_trace_id, "mission": mission}]
- child_records = [{
- "trace_id": sub_trace_id,
- "depth": child_depth,
- "context": apply_policy_to_context({
- **existing.context,
- "agent_depth": child_depth,
- "root_trace_id": root_trace_id,
- }, policy),
- "is_new": False,
- }]
- if task_briefs:
- child_state = ensure_task_protocol(child_records[0]["context"])
- child_state["task_brief"] = task_briefs[0].model_dump()
- existing.context = child_records[0]["context"]
- await store.update_trace(existing.trace_id, context=existing.context)
- else:
- parent_depth, root_trace_id = await _resolve_trace_lineage(store, parent_trace)
- if parent_depth >= policy.max_depth:
- return {
- "status": "failed",
- "error": (
- f"Local Sub-Agent depth limit reached: "
- f"depth={parent_depth}, max={policy.max_depth}, "
- f"mode={policy.mode.value}"
- ),
- }
- async def create_child_traces() -> None:
- child_depth = parent_depth + 1
- for i, task_item in enumerate(tasks):
- task_label = (
- task_briefs[i].objective
- if task_briefs
- else task_item
- )
- resolved_agent_type = agent_type or ("delegate" if single else "explore")
- suffix = "delegate" if single else f"explore-{i+1:03d}"
- stid = generate_sub_trace_id(trace_id, suffix)
- child_context = apply_policy_to_context({
- "created_by_tool": "agent",
- "agent_depth": child_depth,
- "root_trace_id": root_trace_id,
- }, policy)
- if policy.requires_task_protocol:
- child_context["task_protocol"] = ensure_task_protocol({
- "task_protocol": {
- "task_brief": task_briefs[i].model_dump(),
- }
- })
- sub_trace = Trace(
- trace_id=stid,
- mode="agent",
- task=task_label,
- parent_trace_id=trace_id,
- parent_goal_id=goal_id,
- agent_type=resolved_agent_type,
- uid=parent_trace.uid,
- model=parent_trace.model,
- status="running",
- context=child_context,
- created_at=datetime.now(),
- )
- await store.create_trace(sub_trace)
- await store.update_goal_tree(stid, GoalTree(mission=task_label))
- all_sub_trace_ids.append({"trace_id": stid, "mission": task_label})
- child_records.append({
- "trace_id": stid,
- "depth": child_depth,
- "context": child_context,
- "is_new": True,
- "agent_type": resolved_agent_type,
- })
- if single:
- nonlocal sub_trace_id
- sub_trace_id = child_records[0]["trace_id"]
- child_limit = policy.max_children_per_parent
- if child_limit is None:
- await create_child_traces()
- else:
- # Recursive 模式在单进程临界区内完成“计数 -> 创建”,
- # 保证并发批次不会共同突破六个直接孩子。
- async with _LOCAL_AGENT_SPAWN_LOCK:
- existing_children = await store.list_traces(
- parent_trace_id=trace_id,
- created_by_tool="agent",
- limit=child_limit + 1,
- )
- requested_children = len(tasks)
- if len(existing_children) + requested_children > child_limit:
- return {
- "status": "failed",
- "error": (
- "Local child Agent limit exceeded: "
- f"existing={len(existing_children)}, "
- f"requested={requested_children}, max={child_limit}"
- ),
- }
- await create_child_traces()
- await _update_goal_start(
- store, trace_id, goal_id,
- "delegate" if single else "explore",
- all_sub_trace_ids,
- accumulate_sub_trace_ids=policy.accumulate_sub_trace_ids,
- status=("waiting_children" if policy.requires_task_protocol else "in_progress"),
- )
- if policy.requires_task_protocol:
- _project_goal_status(context, goal_id, "waiting_children")
- goal_started = True
- if approved_action and protocol_state is not None:
- protocol_state["next_actions"].pop(0)
- protocol_state["protocol_correction_attempts"] = 0
- await store.update_trace(trace_id, context=parent_trace.context)
- # 创建延迟执行规格。Trace 已按批次预创建,但不会提前创建 coroutine,
- # 因而排队孩子被停止时不会产生未 await coroutine 或模型调用。
- execution_specs = []
- for i, (task_item, msgs, child_record) in enumerate(
- zip(tasks, per_agent_msgs, child_records)
- ):
- cur_stid = child_record["trace_id"]
- child_depth = child_record["depth"]
- task_label = task_briefs[i].objective if task_briefs else task_item
- if child_record["is_new"]:
- await broadcast_sub_trace_started(
- trace_id, cur_stid, goal_id or "",
- child_record["agent_type"], task_label,
- )
- # 注册为活跃协作者
- collab_name = task_label[:30] if single and not continued else (
- f"delegate-{cur_stid[:8]}" if single else f"explore-{i+1}"
- )
- await _update_collaborator(
- store, trace_id,
- name=collab_name, sub_trace_id=cur_stid,
- status="running", summary=task_label[:80],
- )
- # 构建消息
- agent_msgs = list(msgs) + [{"role": "user", "content": task_item}]
- allowed_tools = _get_allowed_tools(context, child_depth, policy)
- debug = getattr(runner, 'debug', False)
- agent_label = (agent_type or ("delegate" if single else f"explore-{i+1}"))
- debug_printer = _make_event_printer(agent_label) if debug else None
- # allowed_tools 已是当前深度的精确白名单。
- on_event = _make_interactive_handler(
- runner, cur_stid, trace_id, debug_printer=debug_printer
- )
- child_config = _make_run_config(
- trace_id=cur_stid,
- agent_type=agent_type or ("delegate" if single else "explore"),
- max_iterations=50,
- model=parent_trace.model if parent_trace else "gpt-4o",
- uid=parent_trace.uid if parent_trace else None,
- tools=allowed_tools,
- tool_groups=[], # tools 是精确白名单,不再合并默认 core 组
- name=task_label[:50],
- skills=skills,
- knowledge=context.get("knowledge_config"),
- context=child_record["context"],
- child_execution_mode=child_execution_mode,
- max_parallel_children=max_parallel_children,
- )
- execution_specs.append({
- "index": i,
- "trace_id": cur_stid,
- "collaborator": collab_name,
- "messages": agent_msgs,
- "config": child_config,
- "on_event": on_event,
- "recursive": policy.mode is AgentMode.RECURSIVE,
- })
- # continue_from 不进入新建临界区,但仍需要恢复 Goal 的运行状态。
- if not goal_started:
- await _update_goal_start(
- store, trace_id, goal_id,
- "delegate" if single else "explore",
- all_sub_trace_ids,
- accumulate_sub_trace_ids=policy.accumulate_sub_trace_ids,
- status=("waiting_children" if policy.requires_task_protocol else "in_progress"),
- )
- if policy.requires_task_protocol:
- _project_goal_status(context, goal_id, "waiting_children")
- if policy.mode is AgentMode.RECURSIVE:
- register_child = getattr(runner, "register_recursive_child", None)
- if register_child:
- for spec in execution_specs:
- register_child(trace_id, spec["trace_id"])
- # 执行
- if single:
- # 单任务直接执行(带异常处理)
- spec = execution_specs[0]
- stid = spec["trace_id"]
- collab_name = spec["collaborator"]
- try:
- result = await _execute_child_spec(spec, runner, store)
- await broadcast_sub_trace_completed(
- trace_id, stid,
- result.get("status", "completed"),
- result.get("summary", ""),
- result.get("stats", {}),
- )
- await _update_collaborator(
- store, trace_id,
- name=collab_name, sub_trace_id=stid,
- status=result.get("status", "completed"),
- summary=result.get("summary", "")[:80],
- )
- formatted = _format_single_result(result, stid, continued)
- if policy.requires_task_protocol:
- reports = await _record_pending_task_reports(
- store,
- parent_trace,
- goal_id,
- [(stid, result)],
- context.get("sequence", 0),
- )
- formatted["task_report"] = reports[0].model_dump()
- _project_goal_status(context, goal_id, "pending_review")
- else:
- await _update_goal_complete(
- store, trace_id, goal_id,
- result.get("status", "completed"),
- formatted["summary"],
- )
- return formatted
- except Exception as e:
- error_msg = str(e)
- await broadcast_sub_trace_completed(
- trace_id, stid, "failed", error_msg, {},
- )
- await _update_collaborator(
- store, trace_id,
- name=collab_name, sub_trace_id=stid,
- status="failed", summary=error_msg[:80],
- )
- failed_result = {
- "mode": "delegate",
- "status": "failed",
- "error": error_msg,
- "sub_trace_id": stid,
- }
- if policy.requires_task_protocol:
- reports = await _record_pending_task_reports(
- store,
- parent_trace,
- goal_id,
- [(stid, failed_result)],
- context.get("sequence", 0),
- )
- failed_result["task_report"] = reports[0].model_dump()
- _project_goal_status(context, goal_id, "pending_review")
- else:
- await _update_goal_complete(
- store, trace_id, goal_id,
- "failed", f"委托任务失败: {error_msg}",
- )
- return failed_result
- else:
- async def execute_captured(
- spec: Dict[str, Any],
- semaphore: Optional[asyncio.Semaphore] = None,
- ):
- try:
- return await _execute_child_spec(spec, runner, store, semaphore)
- except Exception as exc:
- return exc
- if policy.mode is AgentMode.LEGACY:
- # Legacy 冻结旧行为:多任务整批并行,不读取新配置。
- raw_results = await asyncio.gather(*(
- execute_captured(spec) for spec in execution_specs
- ))
- elif child_execution_mode == "parallel":
- semaphore = asyncio.Semaphore(max_parallel_children)
- raw_results = await asyncio.gather(*(
- execute_captured(spec, semaphore) for spec in execution_specs
- ))
- else:
- raw_results = []
- for spec in execution_specs:
- raw_results.append(await execute_captured(spec))
- processed_results = []
- for idx, raw in enumerate(raw_results):
- spec = execution_specs[idx]
- stid = spec["trace_id"]
- collab_name = spec["collaborator"]
- if isinstance(raw, Exception):
- error_result = {
- "status": "failed",
- "summary": f"执行出错: {str(raw)}",
- "stats": {"total_messages": 0, "total_tokens": 0, "total_cost": 0.0},
- }
- processed_results.append(error_result)
- await broadcast_sub_trace_completed(
- trace_id, stid, "failed", str(raw), {},
- )
- await _update_collaborator(
- store, trace_id,
- name=collab_name, sub_trace_id=stid,
- status="failed", summary=str(raw)[:80],
- )
- else:
- processed_results.append(raw)
- await broadcast_sub_trace_completed(
- trace_id, stid,
- raw.get("status", "completed"),
- raw.get("summary", ""),
- raw.get("stats", {}),
- )
- await _update_collaborator(
- store, trace_id,
- name=collab_name, sub_trace_id=stid,
- status=raw.get("status", "completed"),
- summary=raw.get("summary", "")[:80],
- )
- formatted = _format_multi_result(tasks, processed_results, all_sub_trace_ids)
- if policy.requires_task_protocol:
- reports = await _record_pending_task_reports(
- store,
- parent_trace,
- goal_id,
- [
- (entry["trace_id"], processed_results[index])
- for index, entry in enumerate(all_sub_trace_ids)
- ],
- context.get("sequence", 0),
- )
- formatted["task_reports"] = [report.model_dump() for report in reports]
- _project_goal_status(context, goal_id, "pending_review")
- else:
- await _update_goal_complete(
- store, trace_id, goal_id,
- formatted["status"],
- formatted["summary"],
- )
- return formatted
- # ===== 远端 Agent 路由 =====
- async def _run_remote_agent(
- agent_type: str,
- task: str,
- messages: Optional[Messages],
- continue_from: Optional[str],
- skills: Optional[List[str]] = None,
- ) -> Dict[str, Any]:
- """
- 通过 HTTP 调用 KnowHub 服务器上的远端 Agent。
- 远端 Agent 的 tools / model / prompt 由服务器端 preset 决定。
- skills 由 caller 指定,服务器按 agent_type 的白名单过滤。
- """
- import httpx
- payload = {
- "agent_type": agent_type,
- "task": task,
- "messages": messages,
- "continue_from": continue_from,
- "skills": skills,
- }
- api_base = _knowhub_api()
- timeout = _remote_agent_timeout()
- try:
- async with httpx.AsyncClient(timeout=timeout) as client:
- response = await client.post(f"{api_base}/api/agent", json=payload)
- response.raise_for_status()
- result = response.json()
- return {
- "mode": "remote",
- "agent_type": agent_type,
- "sub_trace_id": result.get("sub_trace_id"),
- "status": result.get("status", "completed"),
- "summary": result.get("summary", ""),
- "stats": result.get("stats", {}),
- "error": result.get("error"),
- }
- except httpx.HTTPStatusError as e:
- return {
- "mode": "remote",
- "agent_type": agent_type,
- "status": "failed",
- "error": f"HTTP {e.response.status_code}: {e.response.text[:200]}",
- }
- except Exception as e:
- return {
- "mode": "remote",
- "agent_type": agent_type,
- "status": "failed",
- "error": f"远端调用失败: {type(e).__name__}: {e}",
- }
- # ===== 工具定义 =====
- @tool(description="创建子 Agent 执行任务(本地执行或路由到远端服务器,由 agent_type 决定)", hidden_params=["context"], groups=["core"])
- async def agent(
- task: Optional[Union[str, List[str]]] = None,
- task_brief: Optional[Union[TaskBrief, List[TaskBrief]]] = None,
- messages: Optional[Union[Messages, List[Messages]]] = None,
- continue_from: Optional[str] = None,
- agent_type: Optional[str] = None,
- skills: Optional[List[str]] = None,
- context: Optional[dict] = None,
- ) -> Dict[str, Any]:
- """
- 创建子 Agent 执行任务。
- 路由规则:
- - agent_type 以 "remote_" 开头:HTTP 调用 KnowHub 服务器的 /api/agent(仅单任务,无本地文件访问)
- - 否则本地执行:单任务 str = delegate(全量工具);多任务 List[str] = explore(并行、只读)
- Args:
- task: Legacy/Recursive revision 1 任务描述。
- task_brief: Recursive revision 2 的结构化任务说明。
- messages: 预置消息。1D 列表=所有 agent 共享;2D 列表=per-agent
- continue_from: 继续已有 trace(仅单任务)
- agent_type: 子 Agent 类型。带 "remote_" 前缀走远端;否则本地 preset
- skills: 指定本次调用使用的 skill 列表
- - 本地:附加到 system prompt
- - 远端:由服务器按 agent_type 白名单过滤(如 remote_librarian 允许 ask_strategy / upload_strategy)
- context: 框架自动注入的上下文
- """
- try:
- assert_removed_config_absent()
- except ValueError as exc:
- return {"status": "failed", "error": str(exc)}
- if isinstance(messages, str):
- try:
- messages = json.loads(messages)
- except json.JSONDecodeError as exc:
- return {"status": "failed", "error": f"Invalid messages JSON: {exc}"}
- if messages is not None:
- is_1d = isinstance(messages, list) and all(
- isinstance(message, dict) for message in messages
- )
- is_2d = isinstance(messages, list) and bool(messages) and all(
- isinstance(message_list, list)
- and all(isinstance(message, dict) for message in message_list)
- for message_list in messages
- )
- if not (is_1d or is_2d):
- return {
- "status": "failed",
- "error": "messages must be a 1D or 2D message list without mixed dimensions",
- }
- # 远端路由:agent_type 以 remote_ 开头
- if agent_type and agent_type.startswith(REMOTE_PREFIX):
- if not isinstance(task, str):
- return {"status": "failed", "error": "remote agent 只支持单任务 (task: str)"}
- # 归一化 messages:远端只接受 1D Messages 或 None
- remote_msgs: Optional[Messages] = None
- if messages is not None:
- if messages and isinstance(messages[0], list):
- return {"status": "failed", "error": "remote agent 不支持 2D messages (per-agent)"}
- remote_msgs = messages
- return await _run_remote_agent(
- agent_type=agent_type,
- task=task,
- messages=remote_msgs,
- continue_from=continue_from,
- skills=skills,
- )
- # 本地路径:需要 context
- if not context:
- return {"status": "failed", "error": "context is required"}
- store = context.get("store")
- trace_id = context.get("trace_id")
- goal_id = context.get("goal_id")
- runner = context.get("runner")
- missing = []
- if not store:
- missing.append("store")
- if not trace_id:
- missing.append("trace_id")
- if not runner:
- missing.append("runner")
- if missing:
- return {"status": "failed", "error": f"Missing required context: {', '.join(missing)}"}
- parent_trace = await store.get_trace(trace_id)
- if not parent_trace:
- return {"status": "failed", "error": f"Parent trace not found: {trace_id}"}
- try:
- policy = policy_from_context(parent_trace.context)
- except ValueError as exc:
- return {"status": "failed", "error": str(exc)}
- parsed_task_briefs: Optional[List[TaskBrief]] = None
- if policy.requires_task_protocol:
- if task is not None or task_brief is None:
- return {
- "status": "failed",
- "error": "Recursive revision 2 requires task_brief and does not accept task",
- }
- if isinstance(task_brief, str):
- try:
- task_brief = json.loads(task_brief)
- except json.JSONDecodeError as exc:
- return {"status": "failed", "error": f"Invalid TaskBrief JSON: {exc}"}
- raw_briefs = task_brief if isinstance(task_brief, list) else [task_brief]
- try:
- parsed_task_briefs = [TaskBrief.model_validate(item) for item in raw_briefs]
- except ValidationError as exc:
- return {"status": "failed", "error": f"Invalid TaskBrief: {exc}"}
- tasks = [format_task_brief(brief) for brief in parsed_task_briefs]
- single = len(tasks) == 1
- else:
- if task_brief is not None:
- return {"status": "failed", "error": "task_brief requires Recursive revision 2"}
- if task is None:
- return {"status": "failed", "error": "task is required"}
- # 归一化 task → list(保留 Legacy 字符串 JSON 列表兼容)
- if isinstance(task, str):
- task_str = task.strip()
- if task_str.startswith("[") and task_str.endswith("]"):
- try:
- parsed_task = json.loads(task_str)
- if isinstance(parsed_task, list):
- task = parsed_task
- except (json.JSONDecodeError, TypeError):
- pass
- single = isinstance(task, str)
- tasks = [task] if single else task
- if not tasks:
- return {"status": "failed", "error": "task is required"}
- # 归一化 messages → List[Messages](per-agent)
- if messages is None:
- per_agent_msgs: List[Messages] = [[] for _ in tasks]
- elif messages and isinstance(messages[0], list):
- if len(messages) != len(tasks):
- return {
- "status": "failed",
- "error": (
- "2D messages must contain exactly one message list per task: "
- f"tasks={len(tasks)}, messages={len(messages)}"
- ),
- }
- per_agent_msgs = messages # 2D: per-agent
- else:
- per_agent_msgs = [messages] * len(tasks) # 1D: 共享
- if continue_from and not single:
- return {"status": "failed", "error": "continue_from requires single task"}
- return await _run_agents(
- tasks, per_agent_msgs, continue_from,
- store, trace_id, goal_id, runner, context,
- agent_type=agent_type,
- skills=skills,
- task_briefs=parsed_task_briefs,
- )
- @tool(description="评估目标执行结果是否满足要求", hidden_params=["context"], groups=["core"])
- async def evaluate(
- messages: Optional[Messages] = None,
- target_goal_id: Optional[str] = None,
- continue_from: Optional[str] = None,
- context: Optional[dict] = None,
- ) -> Dict[str, Any]:
- """
- 评估目标执行结果是否满足要求。
- 代码自动从 GoalTree 注入目标描述。模型把执行结果和上下文放在 messages 中。
- Args:
- messages: 执行结果和上下文消息(OpenAI 格式)
- target_goal_id: 要评估的目标 ID(默认当前 goal_id)
- continue_from: 继续已有评估 trace
- context: 框架自动注入的上下文
- """
- if not context:
- return {"status": "failed", "error": "context is required"}
- store = context.get("store")
- trace_id = context.get("trace_id")
- current_goal_id = context.get("goal_id")
- runner = context.get("runner")
- missing = []
- if not store:
- missing.append("store")
- if not trace_id:
- missing.append("trace_id")
- if not runner:
- missing.append("runner")
- if missing:
- return {"status": "failed", "error": f"Missing required context: {', '.join(missing)}"}
- # target_goal_id 默认 context["goal_id"]
- goal_id = target_goal_id or current_goal_id
- # 从 GoalTree 获取目标描述
- goal_desc = await _get_goal_description(store, trace_id, goal_id)
- # 构建 evaluator prompt
- eval_prompt = _build_evaluate_prompt(goal_desc, messages)
- # 获取父 Trace 信息
- parent_trace = await store.get_trace(trace_id)
- if not parent_trace:
- return {"status": "failed", "error": f"Parent trace not found: {trace_id}"}
- try:
- policy = policy_from_context(parent_trace.context)
- except ValueError as exc:
- return {"status": "failed", "error": str(exc)}
- # 处理 continue_from 或创建新 Sub-Trace
- if continue_from:
- existing_trace = await store.get_trace(continue_from)
- if not existing_trace:
- return {"status": "failed", "error": f"Continue-from trace not found: {continue_from}"}
- if existing_trace.parent_trace_id != trace_id:
- return {
- "status": "failed",
- "error": "continue_from must reference a direct child of the current trace",
- }
- if existing_trace.uid != parent_trace.uid:
- return {
- "status": "failed",
- "error": "continue_from trace owner does not match the current trace owner",
- }
- if existing_trace.context.get("created_by_tool") != "evaluate":
- return {
- "status": "failed",
- "error": "continue_from must reference a child created by evaluate",
- }
- try:
- existing_policy = policy_from_context(existing_trace.context)
- except ValueError as exc:
- return {"status": "failed", "error": str(exc)}
- if (
- existing_policy.mode is not policy.mode
- or existing_policy.revision != policy.revision
- ):
- return {
- "status": "failed",
- "error": "continue_from trace Agent mode does not match the current trace",
- }
- parent_depth, expected_root_trace_id = await _resolve_trace_lineage(
- store, parent_trace
- )
- child_depth, root_trace_id = await _resolve_trace_lineage(
- store, existing_trace
- )
- if (
- child_depth != parent_depth + 1
- or root_trace_id != expected_root_trace_id
- ):
- return {
- "status": "failed",
- "error": "continue_from trace lineage does not match the current trace",
- }
- sub_trace_id = continue_from
- goal_tree = await store.get_goal_tree(continue_from)
- mission = goal_tree.mission if goal_tree else eval_prompt
- sub_trace_ids = [{"trace_id": sub_trace_id, "mission": mission}]
- else:
- sub_trace_id = generate_sub_trace_id(trace_id, "evaluate")
- parent_depth, root_trace_id = await _resolve_trace_lineage(store, parent_trace)
- evaluator_context = apply_policy_to_context({
- "created_by_tool": "evaluate",
- "agent_depth": parent_depth + 1,
- "root_trace_id": root_trace_id,
- }, policy)
- sub_trace = Trace(
- trace_id=sub_trace_id,
- mode="agent",
- task=eval_prompt,
- parent_trace_id=trace_id,
- parent_goal_id=current_goal_id,
- agent_type="evaluate",
- uid=parent_trace.uid if parent_trace else None,
- model=parent_trace.model if parent_trace else None,
- status="running",
- context=evaluator_context,
- created_at=datetime.now(),
- )
- await store.create_trace(sub_trace)
- await store.update_goal_tree(sub_trace_id, GoalTree(mission=eval_prompt))
- sub_trace_ids = [{"trace_id": sub_trace_id, "mission": eval_prompt}]
- # 广播 sub_trace_started
- await broadcast_sub_trace_started(
- trace_id, sub_trace_id, current_goal_id or "",
- "evaluate", eval_prompt,
- )
- # 更新主 Goal 为 in_progress
- await _update_goal_start(
- store,
- trace_id,
- current_goal_id,
- "evaluate",
- sub_trace_ids,
- accumulate_sub_trace_ids=policy.accumulate_sub_trace_ids,
- )
- # 注册为活跃协作者
- eval_name = f"评估: {(goal_id or 'unknown')[:20]}"
- await _update_collaborator(
- store, trace_id,
- name=eval_name, sub_trace_id=sub_trace_id,
- status="running", summary=f"评估 Goal {goal_id}",
- )
- # 执行评估
- try:
- # evaluate 使用只读工具 + goal
- allowed_tools = ["read_file", "grep_content", "glob_files", "goal"]
- result = await runner.run_result(
- messages=[{"role": "user", "content": eval_prompt}],
- config=_make_run_config(
- trace_id=sub_trace_id,
- agent_type="evaluate",
- model=parent_trace.model if parent_trace else "gpt-4o",
- uid=parent_trace.uid if parent_trace else None,
- tools=allowed_tools,
- tool_groups=[],
- exclude_tools=["agent", "evaluate", "bash_command"],
- name=f"评估: {goal_id}",
- ),
- on_event=_make_interactive_handler(
- runner, sub_trace_id, trace_id,
- debug_printer=_make_event_printer("evaluate") if getattr(runner, 'debug', False) else None,
- ),
- )
- await broadcast_sub_trace_completed(
- trace_id, sub_trace_id,
- result.get("status", "completed"),
- result.get("summary", ""),
- result.get("stats", {}),
- )
- await _update_collaborator(
- store, trace_id,
- name=eval_name, sub_trace_id=sub_trace_id,
- status=result.get("status", "completed"),
- summary=result.get("summary", "")[:80],
- )
- formatted_summary = result.get("summary", "")
- await _update_goal_complete(
- store, trace_id, current_goal_id,
- result.get("status", "completed"),
- formatted_summary,
- )
- return {
- "mode": "evaluate",
- "sub_trace_id": sub_trace_id,
- "continue_from": bool(continue_from),
- **result,
- "summary": formatted_summary,
- }
- except Exception as e:
- error_msg = str(e)
- await broadcast_sub_trace_completed(
- trace_id, sub_trace_id, "failed", error_msg, {},
- )
- await _update_collaborator(
- store, trace_id,
- name=eval_name, sub_trace_id=sub_trace_id,
- status="failed", summary=error_msg[:80],
- )
- await _update_goal_complete(
- store, trace_id, current_goal_id,
- "failed", f"评估任务失败: {error_msg}",
- )
- return {
- "mode": "evaluate",
- "status": "failed",
- "error": error_msg,
- "sub_trace_id": sub_trace_id,
- }
|