|
|
@@ -38,6 +38,15 @@ from agent.skill.skill_loader import load_skills_from_dir
|
|
|
from agent.tools import ToolRegistry, get_tool_registry
|
|
|
from agent.tools.builtin.knowledge import KnowledgeConfig
|
|
|
from agent.core.memory import MemoryConfig
|
|
|
+from agent.core.presets import get_preset
|
|
|
+from agent.orchestration.models import AgentRole, CompletionPolicy
|
|
|
+from agent.orchestration.policy import (
|
|
|
+ DefaultToolPolicy,
|
|
|
+ ResolvedAgentPolicy,
|
|
|
+ PLANNER_TOOLS,
|
|
|
+ WORKER_TOOLS,
|
|
|
+ VALIDATOR_TOOLS,
|
|
|
+)
|
|
|
from agent.core.prompts import (
|
|
|
DEFAULT_SYSTEM_PREFIX,
|
|
|
TRUNCATION_HINT,
|
|
|
@@ -50,6 +59,7 @@ from agent.core.prompts import (
|
|
|
build_summary_header,
|
|
|
build_tool_interrupted_message,
|
|
|
build_agent_continue_hint,
|
|
|
+ ROLE_CONTRACTS,
|
|
|
)
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
@@ -124,9 +134,11 @@ class RunConfig:
|
|
|
name: Optional[str] = None # 显示名称(空则由 utility_llm 自动生成)
|
|
|
enable_prompt_caching: bool = True # 启用 Anthropic Prompt Caching(仅 Claude 模型有效)
|
|
|
parallel_tool_execution: bool = False # 是否启用并发 Tool Call 执行(慎用,需确保无资源冲突)
|
|
|
+ completion_policy: CompletionPolicy = CompletionPolicy.LEGACY_AUTO
|
|
|
|
|
|
# --- Trace 控制 ---
|
|
|
trace_id: Optional[str] = None # None = 新建
|
|
|
+ new_trace_id: Optional[str] = None # 框架内部:为新 Sub-Trace 预分配 ID
|
|
|
parent_trace_id: Optional[str] = None # 子 Agent 专用
|
|
|
parent_goal_id: Optional[str] = None
|
|
|
|
|
|
@@ -188,6 +200,8 @@ class AgentRunner:
|
|
|
goal_tree: Optional[GoalTree] = None,
|
|
|
debug: bool = False,
|
|
|
logger_name: Optional[str] = None,
|
|
|
+ task_coordinator: Optional[Any] = None,
|
|
|
+ tool_policy: Optional[Any] = None,
|
|
|
):
|
|
|
"""
|
|
|
初始化 AgentRunner
|
|
|
@@ -227,6 +241,8 @@ class AgentRunner:
|
|
|
# 当前 run 的 MemoryConfig(由 run() 根据 RunConfig.memory 设置)
|
|
|
# dream 工具从 context.runner 读取此字段,判断是否 memory-bearing
|
|
|
self._current_memory_config: Optional[MemoryConfig] = None
|
|
|
+ self.task_coordinator = task_coordinator
|
|
|
+ self.tool_policy = tool_policy or DefaultToolPolicy()
|
|
|
|
|
|
# ===== 核心公开方法 =====
|
|
|
|
|
|
@@ -291,12 +307,27 @@ class AgentRunner:
|
|
|
config = config or RunConfig()
|
|
|
trace = None
|
|
|
|
|
|
+ resolved_policy = self._resolve_run_policy(config)
|
|
|
+ if resolved_policy.completion_policy == CompletionPolicy.EXPLICIT_VALIDATION:
|
|
|
+ if self.task_coordinator is None:
|
|
|
+ raise RuntimeError(
|
|
|
+ "explicit_validation requires a TaskCoordinator; call wire_orchestration() first"
|
|
|
+ )
|
|
|
+ config.parallel_tool_execution = False
|
|
|
+ config.max_iterations = resolved_policy.max_iterations
|
|
|
+ config.temperature = resolved_policy.temperature
|
|
|
+
|
|
|
# Memory 模式开关(dream 工具会读取此字段)
|
|
|
self._current_memory_config = config.memory
|
|
|
|
|
|
try:
|
|
|
# Phase 1: PREPARE TRACE
|
|
|
trace, goal_tree, sequence = await self._prepare_trace(messages, config)
|
|
|
+ if (
|
|
|
+ resolved_policy.completion_policy == CompletionPolicy.EXPLICIT_VALIDATION
|
|
|
+ and resolved_policy.role == AgentRole.PLANNER
|
|
|
+ ):
|
|
|
+ await self.task_coordinator.ensure_ledger(trace.trace_id, trace.task or "")
|
|
|
# 注册取消事件
|
|
|
self._cancel_events[trace.trace_id] = asyncio.Event()
|
|
|
yield trace
|
|
|
@@ -397,9 +428,9 @@ class AgentRunner:
|
|
|
|
|
|
status = final_trace.status if final_trace else "unknown"
|
|
|
error = final_trace.error_message if final_trace else None
|
|
|
- summary = last_assistant_text
|
|
|
+ summary = last_assistant_text or (final_trace.result_summary if final_trace else "")
|
|
|
|
|
|
- if not summary:
|
|
|
+ if not summary and status != "completed":
|
|
|
status = "failed"
|
|
|
error = error or "Agent 没有产生 assistant 文本结果"
|
|
|
|
|
|
@@ -509,26 +540,28 @@ class AgentRunner:
|
|
|
config: RunConfig,
|
|
|
) -> Tuple[Trace, Optional[GoalTree], int]:
|
|
|
"""创建新 Trace"""
|
|
|
- trace_id = str(uuid.uuid4())
|
|
|
+ trace_id = config.new_trace_id or str(uuid.uuid4())
|
|
|
|
|
|
# 生成任务名称
|
|
|
task_name = config.name or await self._generate_task_name(messages)
|
|
|
|
|
|
# 准备工具 Schema
|
|
|
- tool_schemas = self._get_tool_schemas(config.tools, config.tool_groups, config.exclude_tools)
|
|
|
+ resolved_policy = self._resolve_run_policy(config)
|
|
|
+ tool_schemas = self._get_run_tool_schemas(config, resolved_policy)
|
|
|
|
|
|
trace_obj = Trace(
|
|
|
trace_id=trace_id,
|
|
|
mode="agent",
|
|
|
task=task_name,
|
|
|
agent_type=config.agent_type,
|
|
|
+ agent_role=resolved_policy.role.value,
|
|
|
parent_trace_id=config.parent_trace_id,
|
|
|
parent_goal_id=config.parent_goal_id,
|
|
|
uid=config.uid,
|
|
|
model=config.model,
|
|
|
tools=tool_schemas,
|
|
|
llm_params={"temperature": config.temperature, **config.extra_llm_params},
|
|
|
- context=config.context,
|
|
|
+ context=dict(config.context),
|
|
|
status="running",
|
|
|
)
|
|
|
|
|
|
@@ -1030,7 +1063,7 @@ class AgentRunner:
|
|
|
attempt += '}' * max(0, open_braces) + ']' * max(0, open_brackets)
|
|
|
result = json.loads(attempt)
|
|
|
if isinstance(result, dict):
|
|
|
- self.log.info(f"[JSON Fix] 成功修复 JSON (suffix={repr(suffix)})")
|
|
|
+ logger.info(f"[JSON Fix] 成功修复 JSON (suffix={repr(suffix)})")
|
|
|
return result
|
|
|
except json.JSONDecodeError:
|
|
|
continue
|
|
|
@@ -1049,7 +1082,8 @@ class AgentRunner:
|
|
|
) -> AsyncIterator[Union[Trace, Message]]:
|
|
|
"""ReAct 循环"""
|
|
|
trace_id = trace.trace_id
|
|
|
- tool_schemas = self._get_tool_schemas(config.tools, config.tool_groups, config.exclude_tools)
|
|
|
+ resolved_policy = self._resolve_run_policy(config, trace)
|
|
|
+ tool_schemas = self._get_run_tool_schemas(config, resolved_policy)
|
|
|
|
|
|
# 当前主路径头节点的 sequence(用于设置 parent_sequence)
|
|
|
head_seq = trace.head_sequence
|
|
|
@@ -1278,7 +1312,14 @@ class AgentRunner:
|
|
|
cache_read_tokens = result.get("cache_read_tokens")
|
|
|
|
|
|
# 周期性自动注入上下文(仅主路径)
|
|
|
- if not side_branch_ctx and iteration % CONTEXT_INJECTION_INTERVAL == 0:
|
|
|
+ if (
|
|
|
+ not side_branch_ctx
|
|
|
+ and iteration % CONTEXT_INJECTION_INTERVAL == 0
|
|
|
+ and (
|
|
|
+ resolved_policy.completion_policy == CompletionPolicy.LEGACY_AUTO
|
|
|
+ or "get_current_context" in resolved_policy.effective_tools
|
|
|
+ )
|
|
|
+ ):
|
|
|
# 检查是否已经调用了 get_current_context
|
|
|
if tool_calls:
|
|
|
has_context_call = any(
|
|
|
@@ -1320,7 +1361,13 @@ class AgentRunner:
|
|
|
self.log.info(f"[Skill 指定注入] 自动添加 skill(\"{skill_name}\") 工具调用")
|
|
|
|
|
|
# 按需自动创建 root goal(仅主路径)
|
|
|
- if not side_branch_ctx and goal_tree and not goal_tree.goals and tool_calls:
|
|
|
+ if (
|
|
|
+ resolved_policy.completion_policy == CompletionPolicy.LEGACY_AUTO
|
|
|
+ and not side_branch_ctx
|
|
|
+ and goal_tree
|
|
|
+ and not goal_tree.goals
|
|
|
+ and tool_calls
|
|
|
+ ):
|
|
|
has_goal_call = any(
|
|
|
tc.get("function", {}).get("name") == "goal"
|
|
|
for tc in tool_calls
|
|
|
@@ -1622,6 +1669,9 @@ class AgentRunner:
|
|
|
})
|
|
|
continue
|
|
|
|
|
|
+ terminal_run = False
|
|
|
+ terminal_summary: Optional[str] = None
|
|
|
+
|
|
|
if tool_calls and config.auto_execute_tools:
|
|
|
history.append({
|
|
|
"role": "assistant",
|
|
|
@@ -1664,9 +1714,21 @@ class AgentRunner:
|
|
|
except ImportError:
|
|
|
pass
|
|
|
try:
|
|
|
- tool_result = await self.tools.execute(
|
|
|
- tool_name, tool_args, uid=config.uid or "",
|
|
|
- context={"store": self.trace_store, "trace_id": trace_id, "goal_id": current_goal_id, "runner": self, "goal_tree": goal_tree, "knowledge_config": config.knowledge, "sequence": sequence, "side_branch": {"type": side_branch_ctx.type, "branch_id": side_branch_ctx.branch_id, "is_side_branch": True, "max_turns": side_branch_ctx.max_turns, "trigger_event": trigger_event_for_tool} if side_branch_ctx else None, **(config.context or {})}
|
|
|
+ tool_result = await self._execute_authorized_tool(
|
|
|
+ tool_name,
|
|
|
+ tool_args,
|
|
|
+ tc["id"],
|
|
|
+ config,
|
|
|
+ trace,
|
|
|
+ goal_tree,
|
|
|
+ sequence,
|
|
|
+ side_branch={
|
|
|
+ "type": side_branch_ctx.type,
|
|
|
+ "branch_id": side_branch_ctx.branch_id,
|
|
|
+ "is_side_branch": True,
|
|
|
+ "max_turns": side_branch_ctx.max_turns,
|
|
|
+ "trigger_event": trigger_event_for_tool,
|
|
|
+ } if side_branch_ctx else None,
|
|
|
)
|
|
|
return (tc, tool_args, tool_result)
|
|
|
except Exception as e:
|
|
|
@@ -1786,30 +1848,29 @@ class AgentRunner:
|
|
|
except ImportError:
|
|
|
pass
|
|
|
|
|
|
- tool_result = await self.tools.execute(
|
|
|
+ tool_result = await self._execute_authorized_tool(
|
|
|
tool_name,
|
|
|
tool_args,
|
|
|
- uid=config.uid or "",
|
|
|
- context={
|
|
|
- "store": self.trace_store,
|
|
|
- "trace_id": trace_id,
|
|
|
- "goal_id": current_goal_id,
|
|
|
- "runner": self,
|
|
|
- "goal_tree": goal_tree,
|
|
|
- "knowledge_config": config.knowledge,
|
|
|
- "sequence": sequence, # 添加sequence用于知识注入记录
|
|
|
- # 新增:侧分支信息
|
|
|
- "side_branch": {
|
|
|
- "type": side_branch_ctx.type,
|
|
|
- "branch_id": side_branch_ctx.branch_id,
|
|
|
- "is_side_branch": True,
|
|
|
- "max_turns": side_branch_ctx.max_turns,
|
|
|
- "trigger_event": trigger_event_for_tool,
|
|
|
- } if side_branch_ctx else None,
|
|
|
- # 合并用户自定义 context(RunConfig.context)
|
|
|
- **(config.context or {}),
|
|
|
- },
|
|
|
+ tc["id"],
|
|
|
+ config,
|
|
|
+ trace,
|
|
|
+ goal_tree,
|
|
|
+ sequence,
|
|
|
+ side_branch={
|
|
|
+ "type": side_branch_ctx.type,
|
|
|
+ "branch_id": side_branch_ctx.branch_id,
|
|
|
+ "is_side_branch": True,
|
|
|
+ "max_turns": side_branch_ctx.max_turns,
|
|
|
+ "trigger_event": trigger_event_for_tool,
|
|
|
+ } if side_branch_ctx else None,
|
|
|
)
|
|
|
+
|
|
|
+ terminal_control = None
|
|
|
+ if isinstance(tool_result, dict):
|
|
|
+ terminal_control = tool_result.get("_control")
|
|
|
+ if terminal_control:
|
|
|
+ tool_result = dict(tool_result)
|
|
|
+ tool_result.pop("_control", None)
|
|
|
|
|
|
# 如果是 goal 工具,记录执行后的状态
|
|
|
if tool_name == "goal" and goal_tree:
|
|
|
@@ -1924,6 +1985,20 @@ class AgentRunner:
|
|
|
except Exception as e:
|
|
|
self.log.warning(f"[Skill 指定注入] 记录追踪失败: {e}")
|
|
|
|
|
|
+ if terminal_control and terminal_control.get("terminate_run"):
|
|
|
+ terminal_run = True
|
|
|
+ terminal_summary = terminal_control.get("result_summary") or tool_text
|
|
|
+ if self.trace_store:
|
|
|
+ await self.trace_store.update_trace(
|
|
|
+ trace_id,
|
|
|
+ result_summary=terminal_summary,
|
|
|
+ head_sequence=head_seq,
|
|
|
+ )
|
|
|
+ break
|
|
|
+
|
|
|
+ if terminal_run:
|
|
|
+ break
|
|
|
+
|
|
|
# on_complete 模式:goal(done=...) 后立即压缩该 goal 的消息
|
|
|
if (
|
|
|
not side_branch_ctx
|
|
|
@@ -2978,6 +3053,100 @@ class AgentRunner:
|
|
|
)
|
|
|
return messages
|
|
|
|
|
|
+ def _resolve_run_policy(
|
|
|
+ self,
|
|
|
+ config: RunConfig,
|
|
|
+ trace: Optional[Trace] = None,
|
|
|
+ ) -> ResolvedAgentPolicy:
|
|
|
+ """Resolve role and effective tools from the preset, never caller context."""
|
|
|
+ preset = get_preset(config.agent_type)
|
|
|
+ policy = self.tool_policy.resolve(config, preset, self.tools)
|
|
|
+ if policy.completion_policy == CompletionPolicy.EXPLICIT_VALIDATION:
|
|
|
+ if policy.role == AgentRole.LEGACY:
|
|
|
+ raise ValueError(
|
|
|
+ f"Preset '{config.agent_type}' has legacy role and cannot run explicit_validation"
|
|
|
+ )
|
|
|
+ if trace and trace.agent_role not in (policy.role.value, "legacy"):
|
|
|
+ raise ValueError(
|
|
|
+ f"Trace role mismatch: stored={trace.agent_role}, requested={policy.role.value}"
|
|
|
+ )
|
|
|
+ return policy
|
|
|
+
|
|
|
+ def _get_run_tool_schemas(
|
|
|
+ self,
|
|
|
+ config: RunConfig,
|
|
|
+ policy: Optional[ResolvedAgentPolicy] = None,
|
|
|
+ ) -> List[Dict]:
|
|
|
+ policy = policy or self._resolve_run_policy(config)
|
|
|
+ if policy.completion_policy == CompletionPolicy.LEGACY_AUTO:
|
|
|
+ schemas = self._get_tool_schemas(config.tools, config.tool_groups, config.exclude_tools)
|
|
|
+ explicit_only = PLANNER_TOOLS | WORKER_TOOLS | VALIDATOR_TOOLS
|
|
|
+ return [
|
|
|
+ schema for schema in schemas
|
|
|
+ if schema.get("function", {}).get("name") not in explicit_only
|
|
|
+ ]
|
|
|
+ return self.tools.get_schemas(sorted(policy.effective_tools))
|
|
|
+
|
|
|
+ def _build_protected_tool_context(
|
|
|
+ self,
|
|
|
+ config: RunConfig,
|
|
|
+ trace: Trace,
|
|
|
+ goal_tree: Optional[GoalTree],
|
|
|
+ sequence: int,
|
|
|
+ side_branch: Optional[Dict[str, Any]] = None,
|
|
|
+ ) -> Dict[str, Any]:
|
|
|
+ """Merge host context first; framework identity fields always win."""
|
|
|
+ context = dict(config.context or {})
|
|
|
+ trace_context = trace.context or {}
|
|
|
+ context.update({
|
|
|
+ "store": self.trace_store,
|
|
|
+ "trace_id": trace.trace_id,
|
|
|
+ "root_trace_id": trace_context.get("root_trace_id", trace.trace_id),
|
|
|
+ "goal_id": goal_tree.current_id if goal_tree else None,
|
|
|
+ "runner": self,
|
|
|
+ "goal_tree": goal_tree,
|
|
|
+ "knowledge_config": config.knowledge,
|
|
|
+ "sequence": sequence,
|
|
|
+ "side_branch": side_branch,
|
|
|
+ "role": trace.agent_role,
|
|
|
+ "completion_policy": CompletionPolicy(config.completion_policy).value,
|
|
|
+ "task_id": trace_context.get("task_id"),
|
|
|
+ "spec_version": trace_context.get("spec_version"),
|
|
|
+ "attempt_id": trace_context.get("attempt_id"),
|
|
|
+ "snapshot_id": trace_context.get("snapshot_id"),
|
|
|
+ "validation_id": trace_context.get("validation_id"),
|
|
|
+ "coordinator": self.task_coordinator,
|
|
|
+ })
|
|
|
+ return context
|
|
|
+
|
|
|
+ async def _execute_authorized_tool(
|
|
|
+ self,
|
|
|
+ tool_name: str,
|
|
|
+ tool_args: Dict[str, Any],
|
|
|
+ tool_call_id: str,
|
|
|
+ config: RunConfig,
|
|
|
+ trace: Trace,
|
|
|
+ goal_tree: Optional[GoalTree],
|
|
|
+ sequence: int,
|
|
|
+ side_branch: Optional[Dict[str, Any]] = None,
|
|
|
+ ) -> Any:
|
|
|
+ policy = self._resolve_run_policy(config, trace)
|
|
|
+ if policy.completion_policy == CompletionPolicy.EXPLICIT_VALIDATION:
|
|
|
+ authorization = self.tool_policy.authorize(policy.role, tool_name, policy)
|
|
|
+ if not authorization.allowed:
|
|
|
+ self.log.warning("Rejected unauthorized tool call: %s", authorization.reason)
|
|
|
+ return {"text": f"Error: {authorization.reason}", "error": "unauthorized_tool"}
|
|
|
+ context = self._build_protected_tool_context(
|
|
|
+ config, trace, goal_tree, sequence, side_branch=side_branch
|
|
|
+ )
|
|
|
+ context["tool_call_id"] = tool_call_id
|
|
|
+ return await self.tools.execute(
|
|
|
+ tool_name,
|
|
|
+ tool_args,
|
|
|
+ uid=config.uid or "",
|
|
|
+ context=context,
|
|
|
+ )
|
|
|
+
|
|
|
def _get_tool_schemas(
|
|
|
self,
|
|
|
tools: Optional[List[str]] = None,
|
|
|
@@ -3077,6 +3246,13 @@ class AgentRunner:
|
|
|
except Exception as e:
|
|
|
self.log.warning(f"[Memory] 加载记忆失败,跳过注入: {e}")
|
|
|
|
|
|
+ # 框架角色契约最后追加,Host prompt、skills 和 memory 都不能覆盖不变量。
|
|
|
+ policy = self._resolve_run_policy(config)
|
|
|
+ if policy.completion_policy == CompletionPolicy.EXPLICIT_VALIDATION:
|
|
|
+ contract = ROLE_CONTRACTS.get(policy.role.value)
|
|
|
+ if contract:
|
|
|
+ system_prompt += f"\n\n{contract}"
|
|
|
+
|
|
|
return system_prompt
|
|
|
|
|
|
async def _generate_task_name(self, messages: List[Dict]) -> str:
|