|
|
@@ -128,6 +128,7 @@ class RunConfig:
|
|
|
enable_prompt_caching: bool = True # 启用 Anthropic Prompt Caching(仅 Claude 模型有效)
|
|
|
parallel_tool_execution: bool = False # 是否启用并发 Tool Call 执行(慎用,需确保无资源冲突)
|
|
|
completion_policy: CompletionPolicy = CompletionPolicy.LEGACY_AUTO
|
|
|
+ root_task_spec: Optional[Dict[str, Any]] = None # explicit Planner 新 Trace 的 Root Task 草案
|
|
|
|
|
|
# --- Trace 控制 ---
|
|
|
trace_id: Optional[str] = None # None = 新建
|
|
|
@@ -300,12 +301,21 @@ class AgentRunner:
|
|
|
config = config or RunConfig()
|
|
|
trace = None
|
|
|
|
|
|
- resolved_policy = self._resolve_run_policy(config)
|
|
|
+ resume_trace = await self._restore_resume_identity(config)
|
|
|
+ resolved_policy = self._resolve_run_policy(config, resume_trace)
|
|
|
+ if resume_trace:
|
|
|
+ self.validate_resume_position(resume_trace, config.after_sequence)
|
|
|
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"
|
|
|
)
|
|
|
+ if resume_trace and resolved_policy.role == AgentRole.PLANNER:
|
|
|
+ await self.task_coordinator.ensure_ledger(
|
|
|
+ resume_trace.trace_id,
|
|
|
+ None,
|
|
|
+ allow_create=False,
|
|
|
+ )
|
|
|
config.parallel_tool_execution = False
|
|
|
config.max_iterations = resolved_policy.max_iterations
|
|
|
config.temperature = resolved_policy.temperature
|
|
|
@@ -316,11 +326,16 @@ class AgentRunner:
|
|
|
try:
|
|
|
# Phase 1: PREPARE TRACE
|
|
|
trace, goal_tree, sequence = await self._prepare_trace(messages, config)
|
|
|
+ resolved_policy = self._resolve_run_policy(config, trace)
|
|
|
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 "")
|
|
|
+ await self.task_coordinator.ensure_ledger(
|
|
|
+ trace.trace_id,
|
|
|
+ config.root_task_spec,
|
|
|
+ allow_create=config.trace_id is None,
|
|
|
+ )
|
|
|
# 注册取消事件
|
|
|
self._cancel_events[trace.trace_id] = asyncio.Event()
|
|
|
yield trace
|
|
|
@@ -423,7 +438,7 @@ class AgentRunner:
|
|
|
error = final_trace.error_message if final_trace else None
|
|
|
summary = last_assistant_text or (final_trace.result_summary if final_trace else "")
|
|
|
|
|
|
- if not summary and status != "completed":
|
|
|
+ if not summary and status not in ("completed", "incomplete", "stopped"):
|
|
|
status = "failed"
|
|
|
error = error or "Agent 没有产生 assistant 文本结果"
|
|
|
|
|
|
@@ -1077,6 +1092,13 @@ class AgentRunner:
|
|
|
trace_id = trace.trace_id
|
|
|
resolved_policy = self._resolve_run_policy(config, trace)
|
|
|
tool_schemas = self._get_run_tool_schemas(config, resolved_policy)
|
|
|
+ explicit_role = (
|
|
|
+ resolved_policy.role
|
|
|
+ if resolved_policy.completion_policy == CompletionPolicy.EXPLICIT_VALIDATION
|
|
|
+ else None
|
|
|
+ )
|
|
|
+ explicit_terminal_submitted = False
|
|
|
+ terminal_summary: Optional[str] = None
|
|
|
|
|
|
# 当前主路径头节点的 sequence(用于设置 parent_sequence)
|
|
|
head_seq = trace.head_sequence
|
|
|
@@ -1663,7 +1685,7 @@ class AgentRunner:
|
|
|
continue
|
|
|
|
|
|
terminal_run = False
|
|
|
- terminal_summary: Optional[str] = None
|
|
|
+ terminal_summary = None
|
|
|
|
|
|
if tool_calls and config.auto_execute_tools:
|
|
|
history.append({
|
|
|
@@ -1989,6 +2011,8 @@ class AgentRunner:
|
|
|
break
|
|
|
|
|
|
if terminal_run:
|
|
|
+ if explicit_role in (AgentRole.WORKER, AgentRole.VALIDATOR):
|
|
|
+ explicit_terminal_submitted = True
|
|
|
break
|
|
|
|
|
|
# on_complete 模式:goal(done=...) 后立即压缩该 goal 的消息
|
|
|
@@ -2029,6 +2053,30 @@ class AgentRunner:
|
|
|
# 如果在侧分支中,已经在上面处理过了(不会走到这里)
|
|
|
# 主路径无工具调用 → 任务完成,检查是否需要完成后反思或知识评估
|
|
|
|
|
|
+ if explicit_role == AgentRole.PLANNER:
|
|
|
+ mission = await self._get_mission_completion(trace_id)
|
|
|
+ mission_status = self._status_value(mission.get("status"))
|
|
|
+ if mission_status in ("completed", "blocked"):
|
|
|
+ break
|
|
|
+
|
|
|
+ control_msg = Message.create(
|
|
|
+ trace_id=trace_id,
|
|
|
+ role="user",
|
|
|
+ sequence=sequence,
|
|
|
+ parent_sequence=head_seq if head_seq > 0 else None,
|
|
|
+ content=self._build_mission_control_message(mission),
|
|
|
+ )
|
|
|
+ if self.trace_store:
|
|
|
+ await self.trace_store.add_message(control_msg)
|
|
|
+ yield control_msg
|
|
|
+ history.append(control_msg.to_llm_dict())
|
|
|
+ head_seq = sequence
|
|
|
+ sequence += 1
|
|
|
+ continue
|
|
|
+
|
|
|
+ if explicit_role in (AgentRole.WORKER, AgentRole.VALIDATOR):
|
|
|
+ break
|
|
|
+
|
|
|
# 检查是否有待评估的知识
|
|
|
if not side_branch_ctx and self.trace_store:
|
|
|
pending = await self.trace_store.get_pending_knowledge_entries(trace_id)
|
|
|
@@ -2054,18 +2102,111 @@ class AgentRunner:
|
|
|
self._context_usage.pop(trace_id, None)
|
|
|
self._saved_knowledge_ids.pop(trace_id, None)
|
|
|
|
|
|
- # 更新 head_sequence 并完成 Trace
|
|
|
+ final_status = "completed"
|
|
|
+ final_error: Optional[str] = None
|
|
|
+ final_summary: Optional[str] = terminal_summary
|
|
|
+
|
|
|
+ if explicit_role == AgentRole.PLANNER:
|
|
|
+ mission = await self._get_mission_completion(trace_id)
|
|
|
+ mission_status = self._status_value(mission.get("status"))
|
|
|
+ if mission_status == "completed":
|
|
|
+ final_summary = mission.get("result_summary") or final_summary
|
|
|
+ elif mission_status == "blocked":
|
|
|
+ final_status = "incomplete"
|
|
|
+ final_error = "mission_blocked"
|
|
|
+ blocked_reason = mission.get("blocked_reason")
|
|
|
+ if blocked_reason:
|
|
|
+ self.log.warning("Mission %s blocked: %s", trace_id, blocked_reason)
|
|
|
+ else:
|
|
|
+ final_status = "incomplete"
|
|
|
+ final_error = "iteration_limit_reached"
|
|
|
+ elif explicit_role in (AgentRole.WORKER, AgentRole.VALIDATOR):
|
|
|
+ if not explicit_terminal_submitted:
|
|
|
+ final_status = "failed"
|
|
|
+ final_error = "protocol_violation"
|
|
|
+
|
|
|
+ # 更新 head_sequence 和 Trace 终态
|
|
|
if self.trace_store:
|
|
|
- await self.trace_store.update_trace(
|
|
|
- trace_id,
|
|
|
- status="completed",
|
|
|
- head_sequence=head_seq,
|
|
|
- completed_at=datetime.now(),
|
|
|
- )
|
|
|
+ updates: Dict[str, Any] = {
|
|
|
+ "status": final_status,
|
|
|
+ "head_sequence": head_seq,
|
|
|
+ "error_message": final_error,
|
|
|
+ "completed_at": datetime.now(),
|
|
|
+ }
|
|
|
+ if final_summary:
|
|
|
+ updates["result_summary"] = final_summary
|
|
|
+ await self.trace_store.update_trace(trace_id, **updates)
|
|
|
trace_obj = await self.trace_store.get_trace(trace_id)
|
|
|
if trace_obj:
|
|
|
yield trace_obj
|
|
|
|
|
|
+ async def _get_mission_completion(self, root_trace_id: str) -> Dict[str, Any]:
|
|
|
+ """Read the coordinator-owned mission completion snapshot."""
|
|
|
+ method = getattr(self.task_coordinator, "mission_completion", None)
|
|
|
+ if method is None:
|
|
|
+ raise RuntimeError("TaskCoordinator does not provide mission_completion()")
|
|
|
+ snapshot = await method(root_trace_id)
|
|
|
+ if not isinstance(snapshot, dict) or "status" not in snapshot:
|
|
|
+ raise RuntimeError("mission_completion() returned an invalid snapshot")
|
|
|
+ return snapshot
|
|
|
+
|
|
|
+ async def _restore_resume_identity(self, config: RunConfig) -> Optional[Trace]:
|
|
|
+ """Restore durable role identity before any existing Trace is mutated."""
|
|
|
+
|
|
|
+ if not config.trace_id:
|
|
|
+ return None
|
|
|
+ if not self.trace_store:
|
|
|
+ raise ValueError("trace_store required for continue/rewind")
|
|
|
+ trace = await self.trace_store.get_trace(config.trace_id)
|
|
|
+ if trace is None:
|
|
|
+ raise ValueError(f"Trace not found: {config.trace_id}")
|
|
|
+ stored_role = AgentRole(trace.agent_role)
|
|
|
+ requested_policy = CompletionPolicy(config.completion_policy)
|
|
|
+ if stored_role == AgentRole.LEGACY:
|
|
|
+ if requested_policy == CompletionPolicy.EXPLICIT_VALIDATION:
|
|
|
+ raise ValueError("A legacy Trace cannot resume as explicit_validation")
|
|
|
+ return trace
|
|
|
+ config.completion_policy = CompletionPolicy.EXPLICIT_VALIDATION
|
|
|
+ if config.agent_type == "default":
|
|
|
+ config.agent_type = trace.agent_type or stored_role.value
|
|
|
+ return trace
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _status_value(status: Any) -> str:
|
|
|
+ value = getattr(status, "value", status)
|
|
|
+ return str(value).lower() if value is not None else ""
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def validate_resume_position(
|
|
|
+ trace: Trace,
|
|
|
+ after_sequence: Optional[int],
|
|
|
+ ) -> None:
|
|
|
+ """Reject rewind for durable explicit Planner mission state."""
|
|
|
+ if (
|
|
|
+ trace.agent_role == AgentRole.PLANNER.value
|
|
|
+ and after_sequence is not None
|
|
|
+ and after_sequence < max(trace.head_sequence, trace.last_sequence)
|
|
|
+ ):
|
|
|
+ raise ValueError(
|
|
|
+ "explicit-validation Planner traces do not support rewind"
|
|
|
+ )
|
|
|
+
|
|
|
+ @staticmethod
|
|
|
+ def _build_mission_control_message(snapshot: Dict[str, Any]) -> str:
|
|
|
+ """Build a compact, framework-owned continuation instruction."""
|
|
|
+ state = {
|
|
|
+ "root_task_id": snapshot.get("root_task_id"),
|
|
|
+ "root_status": AgentRunner._status_value(snapshot.get("status")),
|
|
|
+ "pending_tasks": snapshot.get("pending_tasks") or [],
|
|
|
+ }
|
|
|
+ return (
|
|
|
+ "[FRAMEWORK_MISSION_INCOMPLETE]\n"
|
|
|
+ "The root mission is not complete. Continue the orchestration loop: "
|
|
|
+ "inspect the task ledger, resolve pending work, and accept the root task "
|
|
|
+ "only after its current version has passed validation.\n"
|
|
|
+ f"Mission state: {json.dumps(state, ensure_ascii=False, default=str)}"
|
|
|
+ )
|
|
|
+
|
|
|
# ===== 压缩辅助方法 =====
|
|
|
|
|
|
def _rebuild_history_after_compression(
|
|
|
@@ -3052,15 +3193,15 @@ class AgentRunner:
|
|
|
"""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 trace and trace.agent_role != policy.role.value:
|
|
|
+ raise ValueError(
|
|
|
+ f"Trace role mismatch: stored={trace.agent_role}, requested={policy.role.value}"
|
|
|
+ )
|
|
|
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(
|