|
|
@@ -1,7 +1,8 @@
|
|
|
"""Sub-Agent 的创建、调度、汇合与旧评估工具。
|
|
|
|
|
|
``agent`` 由父 Agent 调用:Legacy 只创建一层,Recursive 还会执行深度、孩子数、
|
|
|
-权限、预算和审核门禁;本地任务进程内运行,``remote_*`` 仍通过 KnowHub HTTP 路由。
|
|
|
+权限、预算和审核门禁;本地任务进程内运行。Legacy 和 Recursive
|
|
|
+revision 1 的 ``remote_*`` 仍通过 KnowHub HTTP 路由,revision 2 失败关闭。
|
|
|
``evaluate`` 保留给 Legacy 和旧的 Recursive revision 1;revision 2 使用框架管理的独立 Validator。
|
|
|
"""
|
|
|
|
|
|
@@ -50,10 +51,16 @@ 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,
|
|
|
+ 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
|
|
|
|
|
|
|
|
|
# ===== 远端路由常量 =====
|
|
|
@@ -1222,11 +1229,33 @@ async def _run_agents(
|
|
|
tool_groups=[], # tools 是精确白名单,不再合并默认 core 组
|
|
|
name=task_label[:50],
|
|
|
skills=skills,
|
|
|
- knowledge=context.get("knowledge_config"),
|
|
|
+ knowledge=context.get("knowledge_config") or KnowledgeConfig(),
|
|
|
context=child_record["context"],
|
|
|
child_execution_mode=child_execution_mode,
|
|
|
max_parallel_children=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 = 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,
|
|
|
@@ -1490,7 +1519,7 @@ async def _run_remote_agent(
|
|
|
|
|
|
# ===== 工具定义 =====
|
|
|
|
|
|
-@tool(description="创建子 Agent 执行任务(本地执行或路由到远端服务器,由 agent_type 决定)", hidden_params=["context"], groups=["core"])
|
|
|
+@tool(description="创建子 Agent 执行任务;Recursive revision 2 仅允许本地结构化委派", hidden_params=["context"], groups=["core"])
|
|
|
async def agent(
|
|
|
task: Optional[Union[str, List[str]]] = None,
|
|
|
task_brief: Optional[Union[TaskBrief, List[TaskBrief]]] = None,
|
|
|
@@ -1504,7 +1533,9 @@ async def agent(
|
|
|
父 Agent 用来创建或续跑直属子 Agent 的公开工具。
|
|
|
|
|
|
路由规则:
|
|
|
- - agent_type 以 "remote_" 开头:HTTP 调用 KnowHub 服务器的 /api/agent(仅单任务,无本地文件访问)
|
|
|
+ - Legacy/Recursive revision 1 中 agent_type 以 "remote_" 开头:HTTP
|
|
|
+ 调用 KnowHub 服务器的 /api/agent(仅单任务,无本地文件访问)
|
|
|
+ - Recursive revision 2 不支持 remote Agent,也不会发出 HTTP 请求
|
|
|
- 否则本地执行:Legacy/Recursive revision 1 使用 ``task``;revision 2 使用 ``task_brief``
|
|
|
|
|
|
Args:
|
|
|
@@ -1512,7 +1543,8 @@ async def agent(
|
|
|
task_brief: Recursive revision 2 的结构化任务说明。
|
|
|
messages: 预置消息。1D 列表=所有 agent 共享;2D 列表=per-agent
|
|
|
continue_from: 继续已有 trace(仅单任务)
|
|
|
- agent_type: 子 Agent 类型。带 "remote_" 前缀走远端;否则本地 preset
|
|
|
+ agent_type: 子 Agent 类型。Legacy/Recursive revision 1 带
|
|
|
+ "remote_" 前缀走远端;否则本地 preset
|
|
|
skills: 指定本次调用使用的 skill 列表
|
|
|
- 本地:附加到 system prompt
|
|
|
- 远端:原样发送,由远端实现决定最终权限
|
|
|
@@ -1523,6 +1555,34 @@ async def agent(
|
|
|
except ValueError as exc:
|
|
|
return {"status": "failed", "error": str(exc)}
|
|
|
|
|
|
+ # 任何路由决策都必须来自已持久化的父 Trace 策略。先看
|
|
|
+ # ``remote_*`` 会让 Recursive revision 2 绕过本地协议、预算与审核门禁。
|
|
|
+ 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)
|
|
|
@@ -1545,6 +1605,14 @@ async def agent(
|
|
|
|
|
|
# 远端路由:agent_type 以 remote_ 开头
|
|
|
if agent_type and agent_type.startswith(REMOTE_PREFIX):
|
|
|
+ if policy.requires_task_protocol:
|
|
|
+ return {
|
|
|
+ "status": "failed",
|
|
|
+ "error": (
|
|
|
+ "Recursive revision 2 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
|
|
|
@@ -1561,32 +1629,6 @@ async def agent(
|
|
|
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)}
|
|
|
if policy.requires_task_protocol and messages is not None:
|
|
|
return {
|
|
|
"status": "failed",
|