| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237 |
- """Sub-Agent 的创建、调度、汇合与旧评估工具。
- ``agent`` 由父 Agent 调用:Legacy 只创建一层,Recursive 还会执行深度、孩子数、
- 权限、预算和审核门禁;本地任务进程内运行。Legacy 和 Recursive
- revision 1 的 ``remote_*`` 仍通过 KnowHub HTTP 路由;受管理的 Structured
- Recursive 失败关闭。``evaluate`` 保留给 Legacy 和旧的 Recursive
- revision 1;Structured Recursive 使用框架管理的独立 Validator。
- """
- import asyncio
- import json
- import os
- from copy import deepcopy
- 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,
- require_mutable_trace_policy,
- validate_recursive_child_execution,
- )
- from cyber_agent.core.task_protocol import (
- TaskBrief,
- TaskReport,
- ensure_task_protocol,
- initialize_task_progress,
- format_task_brief,
- new_task_protocol,
- pending_review_entry,
- protocol_error_report,
- replace_task_brief,
- stopped_task_report,
- )
- from cyber_agent.core.task_protocol_service import TaskProtocolService
- from cyber_agent.core.context_policy import (
- CONTEXT_ACCESS_KEY,
- ContextPolicyError,
- build_child_context_access,
- context_ref_descriptors,
- normalize_task_brief,
- persist_root_task_anchor,
- require_matching_root_task_anchor,
- task_briefs_match,
- )
- from cyber_agent.core.resource_budget import (
- ResourceBudgetExceeded,
- ResourceBudgetStateError,
- )
- from cyber_agent.core.memory import compute_memory_identity
- from cyber_agent.core.run_snapshot import (
- RunConfigSnapshotV1,
- RunConfigSnapshotV2,
- persist_run_config_snapshot,
- )
- 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
- from cyber_agent.tools.builtin.knowledge import KnowledgeConfig
- # ===== 远端路由常量 =====
- 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,
- *,
- task_protocol_service: TaskProtocolService | None = None,
- ) -> TaskReport:
- """读取孩子已提交的报告,或为异常、停止生成框架报告。
- 子任务执行结束后由报告汇合阶段调用,将异常和停止统一转成可审核的 ``TaskReport``。
- """
- service = task_protocol_service or TaskProtocolService(store)
- async with service.transaction(child_trace_id):
- 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
- state["task_report_progress_revision"] = state.get(
- "task_progress_head_revision"
- )
- state["task_report_validation"] = None
- await store.update_trace(child_trace_id, context=child.context)
- return report
- async def _record_pending_task_reports(
- store,
- runner,
- parent_trace: Trace,
- goal_id: str | None,
- child_results: List[tuple[str, Dict[str, Any]]],
- received_at_sequence: int,
- ) -> List[TaskReport]:
- """验收一批子 Agent 结果并写入父 Trace 的待审核队列。
- ``_run_agents`` 汇合孩子后按输入顺序调用,必要时触发 Validator 并更新 Goal 状态。
- """
- service = getattr(runner, "task_protocol_service", None) or TaskProtocolService(store)
- reports: List[TaskReport] = []
- pending_entries: dict[str, dict[str, Any]] = {}
- 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,
- task_protocol_service=service,
- )
- child_trace = await store.get_trace(child_trace_id)
- if not child_trace:
- raise ValueError(f"Child Trace not found for validation: {child_trace_id}")
- child_state = ensure_task_protocol(child_trace.context)
- task_brief = child_state.get("task_brief")
- report_payload = report.model_dump()
- if report.outcome in {"satisfied", "partial"}:
- validation_run = await runner.validate_recursive_trace(
- child_trace_id,
- scope="task",
- task_brief=task_brief,
- task_report=report_payload,
- )
- else:
- is_protocol_error = report.outcome == "protocol_error"
- validation_run = await runner.validate_recursive_trace(
- child_trace_id,
- scope="task",
- task_brief=task_brief,
- task_report=report_payload,
- deterministic_failure={
- "outcome": "error" if is_protocol_error else "failed",
- "reason": report.summary,
- "issues": report.remaining_issues or [report.summary],
- "retry_from": None if is_protocol_error else "task_definition",
- },
- )
- validation_result = validation_run.result
- candidate_validations = []
- for candidate_ref in report.candidate_refs:
- candidate_run = await runner.validate_recursive_trace(
- child_trace_id,
- task_brief=task_brief,
- task_report=report_payload,
- candidate_ref=candidate_ref,
- )
- candidate_validations.append({
- "candidate_ref": candidate_ref.model_dump(mode="json"),
- "validation_result": candidate_run.result.model_dump(mode="json"),
- })
- pending_entry = pending_review_entry(
- goal_id=goal_id,
- report=report,
- validation_result=validation_result.model_dump(),
- received_at_sequence=received_at_sequence,
- )
- pending_entry["candidate_validations"] = candidate_validations
- pending_entries[child_trace_id] = pending_entry
- reports.append(report)
- async with service.transaction(parent_trace.trace_id):
- fresh_parent = await store.get_trace(parent_trace.trace_id)
- if fresh_parent is None:
- raise ValueError(f"Parent Trace not found: {parent_trace.trace_id}")
- state = ensure_task_protocol(fresh_parent.context)
- state["pending_reviews"].update(pending_entries)
- await store.update_trace(
- parent_trace.trace_id,
- context=fresh_parent.context,
- )
- parent_trace.context = fresh_parent.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]:
- """按调度规格延迟启动一个子 Agent。
- ``_run_agents`` 串行执行或在并行信号量内调用;排队期间停止则不进入 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]]:
- """从隐藏 Tool Context 读取并校验父 Agent 的冻结权限快照。
- ``_run_agents`` 和子级权限计算共用;快照缺失或格式不合法时 Recursive 拒绝创建孩子。
- """
- 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]]:
- """按父级能力、子级固定规则和当前深度计算有效工具集合。
- 创建子 Runner 前调用;Recursive 权限只能逐层收紧,最深层会移除 ``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
- return _filter_allowed_tools(allowed_tools, agent_depth, policy)
- return None
- def _filter_allowed_tools(
- tool_names: set[str],
- agent_depth: int,
- policy: AgentPolicy,
- ) -> list[str]:
- """Apply framework protocol/depth restrictions to one capability set."""
- blocked_tools = {"evaluate", "bash_command"}
- if not policy.requires_task_protocol:
- blocked_tools.update({
- "submit_task_report",
- "review_task_result",
- "update_task_progress",
- "read_context_ref",
- })
- elif not policy.requires_task_progress:
- blocked_tools.add("update_task_progress")
- if policy.mode is AgentMode.LEGACY or agent_depth >= policy.max_depth:
- blocked_tools.add("agent")
- return sorted(tool_names - blocked_tools)
- 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
- async def _load_root_task_anchor(store, trace: Trace):
- """从权威根 Trace读取 Anchor,并校验当前节点持有相同副本。"""
- root_trace_id = trace.context.get("root_trace_id") or trace.trace_id
- root = trace if root_trace_id == trace.trace_id else await store.get_trace(root_trace_id)
- if not root:
- raise ContextPolicyError(f"Recursive root Trace not found: {root_trace_id}")
- anchor = require_matching_root_task_anchor(root.context, trace.context)
- return root, anchor
- 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]:
- """
- 本地 Sub-Agent 的统一创建、调度和结果汇合入口。
- ``agent`` 完成参数归一化后调用;此处执行 Spawn Guard、权限交集、串并行调度和
- 报告汇合,并始终使用父 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 = require_mutable_trace_policy(parent_trace.context)
- except ValueError as exc:
- return {"status": "failed", "error": str(exc)}
- protocol_service = (
- getattr(runner, "task_protocol_service", None)
- or TaskProtocolService(store)
- )
- application_binding = getattr(runner, "application_binding", None)
- application_ref = parent_trace.context.get("application_ref")
- target_role_binding = None
- parent_role_binding = None
- target_role_id = agent_type
- if application_ref is not None:
- if application_binding is None:
- return {
- "status": "failed",
- "error": "Application child creation requires the bound ApplicationRuntime",
- }
- if (
- application_ref
- != application_binding.application_ref.model_dump(mode="json")
- ):
- return {
- "status": "failed",
- "error": "Parent ApplicationRef does not match Runner binding",
- }
- if skills is not None:
- return {
- "status": "failed",
- "error": "Application roles do not allow per-call skill overrides",
- }
- parent_role_id = parent_trace.context.get("application_role_id")
- try:
- parent_role_binding = application_binding.role(parent_role_id)
- except ValueError as exc:
- return {"status": "failed", "error": str(exc)}
- allowed_child_roles = parent_role_binding.role.allowed_child_roles
- if target_role_id is None and len(allowed_child_roles) == 1:
- target_role_id = allowed_child_roles[0]
- if target_role_id not in allowed_child_roles:
- return {
- "status": "failed",
- "error": (
- f"Application role {parent_role_id} cannot create child role: "
- f"{target_role_id}"
- ),
- }
- try:
- target_role_binding = application_binding.role(target_role_id)
- except ValueError as exc:
- return {"status": "failed", "error": str(exc)}
- root_task_anchor = None
- if policy.requires_task_protocol:
- try:
- _, root_task_anchor = await _load_root_task_anchor(
- store,
- parent_trace,
- )
- except ContextPolicyError 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)}
- effective_limits = parent_trace.context.get("effective_run_limits") or {}
- if effective_limits:
- max_parallel_children = min(
- max_parallel_children,
- int(effective_limits["max_parallel_children"]),
- )
- protocol_state = None
- approved_action = None
- if policy.requires_task_protocol:
- protocol_state = ensure_task_protocol(parent_trace.context)
- parent_task_brief = protocol_state.get("task_brief")
- 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": "Writable Recursive runs require 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",
- }
- try:
- matches_approved = not expected_brief or task_briefs_match(
- task_briefs[0],
- expected_brief,
- parent_task_brief=parent_task_brief,
- root_task_anchor=root_task_anchor,
- )
- except ContextPolicyError as exc:
- return {
- "status": "failed",
- "error": f"Approved Recursive TaskBrief is invalid; recreate the trace: {exc}",
- }
- if not matches_approved:
- 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",
- }
- try:
- matches_approved = bool(expected_brief) and task_briefs_match(
- task_briefs[0],
- expected_brief,
- parent_task_brief=parent_task_brief,
- root_task_anchor=root_task_anchor,
- )
- except ContextPolicyError as exc:
- return {
- "status": "failed",
- "error": f"Approved Recursive TaskBrief is invalid; recreate the trace: {exc}",
- }
- if not matches_approved:
- 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]] = []
- created_trace_ids: list[str] = []
- async def fail_created_children(error: Exception) -> None:
- """将已创建但未进入执行阶段的 Recursive 孩子收敛为失败。"""
- for created_trace_id in created_trace_ids:
- created = await store.get_trace(created_trace_id)
- if created and created.status == "running":
- await store.update_trace(
- created_trace_id,
- status="failed",
- error_message=f"Child initialization failed: {error}",
- completed_at=datetime.now(),
- )
- async def run_initialization(operation):
- """执行一次运行前持久化,失败时统一清理已创建孩子。"""
- try:
- return await operation
- except Exception as exc:
- if policy.requires_task_protocol:
- await fail_created_children(exc)
- raise
- 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",
- }
- if target_role_binding is not None and (
- existing.context.get("application_ref") != application_ref
- or existing.context.get("application_role_id")
- != target_role_binding.role.role_id
- or existing.context.get("application_role_hash")
- != target_role_binding.role_hash
- ):
- return {
- "status": "failed",
- "error": "continue_from application role binding does not match",
- }
- 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",
- }
- effective_max_depth = int(
- (parent_trace.context.get("effective_run_limits") or {}).get(
- "max_depth",
- policy.max_depth,
- )
- )
- if child_depth > min(policy.max_depth, effective_max_depth):
- return {
- "status": "failed",
- "error": (
- "continue_from trace exceeds the persisted Agent mode depth: "
- f"depth={child_depth}, max={min(policy.max_depth, effective_max_depth)}"
- ),
- }
- all_sub_trace_ids = [{"trace_id": sub_trace_id, "mission": mission}]
- child_context = apply_policy_to_context({
- **existing.context,
- "agent_depth": child_depth,
- "root_trace_id": root_trace_id,
- }, policy)
- child_records = [{
- "trace_id": sub_trace_id,
- "depth": child_depth,
- "context": child_context,
- "is_new": False,
- }]
- if task_briefs:
- assert root_task_anchor is not None
- prepared_context = deepcopy(child_context)
- child_state = ensure_task_protocol(prepared_context)
- normalized_dump = task_briefs[0].model_dump(mode="json")
- new_context_access = build_child_context_access(
- parent_context=parent_trace.context,
- parent_trace_id=parent_trace.trace_id,
- root_trace_id=root_trace_id,
- uid=parent_trace.uid,
- parent_task_state=protocol_state,
- child_task_brief=task_briefs[0],
- root_task_anchor=root_task_anchor,
- granted_at_sequence=(existing.last_sequence or 0) + 1,
- )
- brief_changed = child_state.get("task_brief") != normalized_dump
- if brief_changed:
- replace_task_brief(
- child_state,
- task_briefs[0],
- effective_at_sequence=(existing.last_sequence or 0) + 1,
- )
- prepared_context[CONTEXT_ACCESS_KEY] = new_context_access
- persist_root_task_anchor(prepared_context, root_task_anchor)
- if (
- target_role_binding is not None
- and getattr(runner, "context_provider", None) is not None
- and brief_changed
- ):
- from cyber_agent.application.context import load_application_context
- from cyber_agent.application.ports import ContextRequest
- await load_application_context(
- application_binding,
- prepared_context,
- ContextRequest(
- application_ref=application_binding.application_ref,
- root_trace_id=root_trace_id,
- trace_id=existing.trace_id,
- parent_trace_id=trace_id,
- uid=parent_trace.uid,
- role_id=target_role_binding.role.role_id,
- task_brief=task_briefs[0],
- task_brief_revision=child_state["task_brief_version"],
- authorized_context_refs=tuple(
- task_briefs[0].context_refs
- ),
- ),
- root_task_anchor=root_task_anchor,
- task_brief=task_briefs[0],
- granted_at_sequence=(existing.last_sequence or 0) + 1,
- )
- async with protocol_service.transaction(existing.trace_id):
- fresh_child = await store.get_trace(existing.trace_id)
- if fresh_child is None:
- return {
- "status": "failed",
- "error": f"continue_from Trace not found: {existing.trace_id}",
- }
- fresh_context = apply_policy_to_context({
- **fresh_child.context,
- "agent_depth": child_depth,
- "root_trace_id": root_trace_id,
- }, policy)
- fresh_state = ensure_task_protocol(fresh_context)
- if fresh_state.get("task_brief") != normalized_dump:
- replace_task_brief(
- fresh_state,
- task_briefs[0],
- effective_at_sequence=(fresh_child.last_sequence or 0) + 1,
- )
- fresh_context[CONTEXT_ACCESS_KEY] = deepcopy(
- prepared_context[CONTEXT_ACCESS_KEY]
- )
- persist_root_task_anchor(fresh_context, root_task_anchor)
- await store.update_trace(
- fresh_child.trace_id,
- context=fresh_context,
- )
- fresh_child.context = fresh_context
- existing = fresh_child
- child_context = fresh_context
- child_records[0]["context"] = fresh_context
- else:
- parent_depth, root_trace_id = await _resolve_trace_lineage(store, parent_trace)
- effective_max_depth = int(
- (parent_trace.context.get("effective_run_limits") or {}).get(
- "max_depth",
- policy.max_depth,
- )
- )
- if parent_depth >= min(policy.max_depth, effective_max_depth):
- return {
- "status": "failed",
- "error": (
- f"Local Sub-Agent depth limit reached: "
- f"depth={parent_depth}, max={min(policy.max_depth, effective_max_depth)}, "
- f"mode={policy.mode.value}"
- ),
- }
- prepared_context_accesses: list[dict[str, Any]] = []
- if policy.requires_task_protocol:
- assert root_task_anchor is not None and task_briefs is not None
- try:
- prepared_context_accesses = [
- build_child_context_access(
- parent_context=parent_trace.context,
- parent_trace_id=parent_trace.trace_id,
- root_trace_id=root_trace_id,
- uid=parent_trace.uid,
- parent_task_state=protocol_state,
- child_task_brief=brief,
- root_task_anchor=root_task_anchor,
- )
- for brief in task_briefs
- ]
- except ContextPolicyError as exc:
- return {
- "status": "failed",
- "error": f"Invalid TaskBrief context references: {exc}",
- }
- 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 = target_role_id 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:
- assert root_task_anchor is not None
- child_context["task_protocol"] = new_task_protocol(
- task_briefs[i]
- )
- child_context[CONTEXT_ACCESS_KEY] = prepared_context_accesses[i]
- persist_root_task_anchor(child_context, root_task_anchor)
- if policy.requires_task_progress:
- initialize_task_progress(child_context["task_protocol"])
- if target_role_binding is not None:
- parent_limits = parent_trace.context["effective_run_limits"]
- role_limits = target_role_binding.effective_limits.model_dump(
- mode="json"
- )
- child_limits = {
- name: min(parent_limits[name], value)
- for name, value in role_limits.items()
- }
- child_context.update({
- "application_ref": application_ref,
- "application_role_id": target_role_binding.role.role_id,
- "application_role_hash": target_role_binding.role_hash,
- "effective_run_limits": child_limits,
- })
- if getattr(runner, "context_provider", None) is not None:
- from cyber_agent.application.context import (
- load_application_context,
- )
- from cyber_agent.application.ports import ContextRequest
- await load_application_context(
- application_binding,
- child_context,
- ContextRequest(
- application_ref=application_binding.application_ref,
- root_trace_id=root_trace_id,
- trace_id=stid,
- parent_trace_id=trace_id,
- uid=parent_trace.uid,
- role_id=target_role_binding.role.role_id,
- task_brief=task_briefs[i],
- task_brief_revision=1,
- authorized_context_refs=tuple(
- task_briefs[i].context_refs
- ),
- ),
- root_task_anchor=root_task_anchor,
- task_brief=task_briefs[i],
- granted_at_sequence=0,
- )
- 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=(
- target_role_binding.role.model
- if target_role_binding is not None
- else parent_trace.model
- ),
- status="running",
- context=child_context,
- created_at=datetime.now(),
- )
- await store.create_trace(sub_trace)
- # A successfully persisted Trace has consumed its Agent budget,
- # even if a later GoalTree write fails.
- created_trace_ids.append(stid)
- 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
- effective_child_limit = (
- parent_trace.context.get("effective_run_limits") or {}
- ).get("max_children_per_parent")
- if effective_child_limit is not None:
- child_limit = min(
- child_limit if child_limit is not None else effective_child_limit,
- int(effective_child_limit),
- )
- 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}"
- ),
- }
- resolved_budget = None
- if policy.requires_task_protocol:
- resolved_budget = await runner._resource_budget_for_trace(trace_id)
- reserved_agents = 0
- if resolved_budget is not None:
- root_trace_id, budget = resolved_budget
- if not runner.resource_budget:
- return {
- "status": "failed",
- "error": "ResourceBudgetController is unavailable",
- }
- try:
- await runner.resource_budget.reserve_agents(
- root_trace_id,
- budget,
- requested_children,
- )
- reserved_agents = requested_children
- except (ResourceBudgetExceeded, ResourceBudgetStateError) as exc:
- return {"status": "failed", "error": str(exc)}
- try:
- await create_child_traces()
- except Exception as exc:
- created_count = len(created_trace_ids)
- uncreated = max(0, reserved_agents - created_count)
- if uncreated:
- await runner.resource_budget.release_agents(
- root_trace_id,
- budget,
- uncreated,
- )
- await fail_created_children(exc)
- raise
- await run_initialization(
- _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:
- try:
- async with protocol_service.transaction(trace_id):
- fresh_parent = await store.get_trace(trace_id)
- if fresh_parent is None:
- raise ValueError(f"Trace not found: {trace_id}")
- fresh_state = ensure_task_protocol(fresh_parent.context)
- if not fresh_state["next_actions"]:
- raise ValueError("Approved next action was already consumed")
- if fresh_state["next_actions"][0] != approved_action:
- raise ValueError(
- "Approved next action changed before execution"
- )
- fresh_state["next_actions"].pop(0)
- fresh_state["protocol_correction_attempts"] = 0
- await store.update_trace(trace_id, context=fresh_parent.context)
- parent_trace.context = fresh_parent.context
- except Exception as exc:
- await fail_created_children(exc)
- raise
- # 创建延迟执行规格。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 run_initialization(
- 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 run_initialization(
- _update_collaborator(
- store, trace_id,
- name=collab_name, sub_trace_id=cur_stid,
- status="running", summary=task_label[:80],
- )
- )
- # 构建消息
- if policy.requires_task_protocol:
- assert task_briefs is not None and root_task_anchor is not None
- task_item = format_task_brief(
- task_briefs[i],
- available_context_refs=context_ref_descriptors(
- child_record["context"]
- ),
- )
- agent_msgs = list(msgs) + [{"role": "user", "content": task_item}]
- allowed_tools = _get_allowed_tools(context, child_depth, policy)
- if target_role_binding is not None:
- assert parent_role_binding is not None
- allowed_tools = _filter_allowed_tools(
- set(parent_role_binding.delegated_tool_names)
- & set(target_role_binding.tool_names),
- 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") or KnowledgeConfig(),
- context=child_record["context"],
- child_execution_mode=child_execution_mode,
- max_parallel_children=max_parallel_children,
- )
- if target_role_binding is not None:
- application_binding.configure_run_config(
- child_config,
- target_role_binding.role.role_id,
- )
- child_config.tools = allowed_tools
- child_config.context = child_record["context"]
- child_config.child_execution_mode = child_execution_mode
- child_config.effective_run_limits = dict(
- child_record["context"]["effective_run_limits"]
- )
- child_config.max_iterations = child_config.effective_run_limits[
- "max_iterations"
- ]
- child_config.max_parallel_children = child_config.effective_run_limits[
- "max_parallel_children"
- ]
- # Recursive children are persisted before their coroutine is scheduled so
- # the parent can account for queued work. Bind the immutable run snapshot
- # before execution; otherwise the first local resume would look exactly
- # like an unsafe pre-snapshot historical Recursive trace.
- if child_record["is_new"]:
- persisted_child = await store.get_trace(cur_stid)
- if not persisted_child:
- raise RuntimeError(f"pre-created child Trace disappeared: {cur_stid}")
- child_memory_identity = (
- compute_memory_identity(child_config.memory)
- if child_config.memory
- else None
- )
- child_snapshot = (
- RunConfigSnapshotV2.from_run_config(
- child_config,
- memory_identity=child_memory_identity,
- )
- if target_role_binding is not None
- else RunConfigSnapshotV1.from_run_config(
- child_config,
- memory_identity=child_memory_identity,
- )
- )
- persist_run_config_snapshot(persisted_child.context, child_snapshot)
- await store.update_trace(
- cur_stid,
- context=persisted_child.context,
- )
- 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 run_initialization(
- _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,
- runner,
- parent_trace,
- goal_id,
- [(stid, result)],
- context.get("sequence", 0),
- )
- formatted["task_report"] = reports[0].model_dump()
- formatted["validation_result"] = ensure_task_protocol(
- parent_trace.context
- )["pending_reviews"][stid]["validation_result"]
- _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,
- runner,
- parent_trace,
- goal_id,
- [(stid, failed_result)],
- context.get("sequence", 0),
- )
- failed_result["task_report"] = reports[0].model_dump()
- failed_result["validation_result"] = ensure_task_protocol(
- parent_trace.context
- )["pending_reviews"][stid]["validation_result"]
- _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,
- runner,
- 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]
- pending = ensure_task_protocol(parent_trace.context)["pending_reviews"]
- formatted["validation_results"] = [
- pending[entry["trace_id"]]["validation_result"]
- for entry in all_sub_trace_ids
- ]
- _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 指定并原样发给服务器,最终权限由远端实现决定。
- """
- 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 执行任务;可写 Recursive 协议仅允许本地结构化委派", 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 的公开工具。
- 路由规则:
- - Legacy/Recursive revision 1 中 agent_type 以 "remote_" 开头:HTTP
- 调用 KnowHub 服务器的 /api/agent(仅单任务,无本地文件访问)
- - Structured Recursive 不支持 remote Agent,也不会发出 HTTP 请求
- - 否则本地执行:Legacy/Recursive revision 1 使用 ``task``;Structured Recursive 使用 ``task_brief``
- Args:
- task: Legacy/Recursive revision 1 任务描述。
- task_brief: Structured Recursive 的结构化任务说明。
- messages: 预置消息。1D 列表=所有 agent 共享;2D 列表=per-agent
- continue_from: 继续已有 trace(仅单任务)
- agent_type: 子 Agent 类型。Legacy/Recursive revision 1 带
- "remote_" 前缀走远端;否则本地 preset
- skills: 指定本次调用使用的 skill 列表
- - 本地:附加到 system prompt
- - 远端:原样发送,由远端实现决定最终权限
- context: 框架自动注入的上下文
- """
- try:
- assert_removed_config_absent()
- except ValueError as exc:
- return {"status": "failed", "error": str(exc)}
- # 任何路由决策都必须来自已持久化的父 Trace 策略。先看
- # ``remote_*`` 会让 Structured Recursive 绕过本地协议、预算与审核门禁。
- 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)}
- 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 policy.requires_task_protocol:
- return {
- "status": "failed",
- "error": (
- "Writable Recursive protocol does not support remote agents; "
- "use a local task_brief delegation"
- ),
- }
- 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,
- )
- if policy.requires_task_protocol and messages is not None:
- return {
- "status": "failed",
- "error": "Writable Recursive protocol does not accept messages; pass bounded context in task_brief",
- }
- 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": "Writable Recursive runs require task_brief and do 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:
- _, root_task_anchor = await _load_root_task_anchor(
- store,
- parent_trace,
- )
- parent_state = ensure_task_protocol(parent_trace.context)
- parent_task_brief = parent_state.get("task_brief")
- parsed_task_briefs = [
- normalize_task_brief(
- item,
- parent_task_brief=parent_task_brief,
- root_task_anchor=root_task_anchor,
- )
- for item in raw_briefs
- ]
- except (ContextPolicyError, ValidationError) as exc:
- return {"status": "failed", "error": f"Invalid TaskBrief: {exc}"}
- tasks = [brief.objective for brief in parsed_task_briefs]
- single = len(tasks) == 1
- else:
- if task_brief is not None:
- return {"status": "failed", "error": "task_brief requires a writable Recursive run"}
- 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)}
- if policy.requires_task_protocol:
- return {
- "status": "failed",
- "error": "evaluate is unavailable in Recursive mode; validation is framework-managed",
- }
- # 处理 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,
- }
|