"""Internal Task execution planner for single-agent task attempts. Team execution is now started explicitly through local Team Workflow MCP tools. This planner only records why the normal Task attempt should continue as a single root-agent run. """ from __future__ import annotations import os from dataclasses import dataclass, field from typing import Any, Literal from beaver.coordinator.models import ExecutionGraph from beaver.engine.context import SkillContext from beaver.engine.providers import ProviderBundle from .models import TaskRecord from .skill_resolver import SkillResolutionReport TaskExecutionMode = Literal["single", "team"] def _agent_team_enabled() -> bool: return os.getenv("BEAVER_AGENT_TEAM_ENABLED", "1").strip().lower() not in {"0", "false", "no", "off"} @dataclass(slots=True) class TaskExecutionPlan: mode: TaskExecutionMode reason: str = "" graph: ExecutionGraph | None = None final_synthesis_instruction: str = "" fallback_error: str | None = None skill_resolution_report: list[SkillResolutionReport] = field(default_factory=list) planner_adaptation: dict[str, Any] = field(default_factory=dict) @property def is_team(self) -> bool: return self.mode == "team" and self.graph is not None @classmethod def single( cls, reason: str, *, fallback_error: str | None = None, planner_adaptation: dict[str, Any] | None = None, ) -> "TaskExecutionPlan": return cls( mode="single", reason=reason, fallback_error=fallback_error, planner_adaptation=dict(planner_adaptation or {}), ) def to_event_payload(self) -> dict[str, Any]: strategy = self.graph.strategy if self.graph is not None else None nodes = self.graph.nodes if self.graph is not None else [] return { "plan_mode": self.mode, "reason": self.reason, "strategy": strategy, "node_ids": [node.node_id for node in nodes], "skill_queries": [ str(node.agent.metadata.get("skill_query") or "") for node in nodes ], "selected_skill_names": [ name for node in nodes for name in node.inherited_pinned_skills ], "ephemeral_guidance_ids": [ item.ephemeral_guidance_id for item in self.skill_resolution_report if item.ephemeral_guidance_id ], "skill_resolution_report": [item.to_dict() for item in self.skill_resolution_report], "planner_adaptation": dict(self.planner_adaptation), "fallback_error": self.fallback_error, } class TaskExecutionPlanner: """Return the current Task execution mode for the root AgentLoop.""" async def plan( self, *, task: TaskRecord, user_message: str, attempt_index: int, provider_bundle: ProviderBundle | None = None, timeout_seconds: float = 30.0, skill_summaries: list[str] | None = None, tool_hints: list[str] | None = None, activated_skills: list[SkillContext] | None = None, ) -> TaskExecutionPlan: if not _agent_team_enabled(): return TaskExecutionPlan.single("planner_disabled_by_environment") if not self._needs_team_planning(task=task, user_message=user_message): return TaskExecutionPlan.single("planner_skipped_simple_task") return TaskExecutionPlan.single("planner_team_replaced_by_workflow_tools") @staticmethod def _needs_team_planning(*, task: TaskRecord, user_message: str) -> bool: text = " ".join( part for part in ( task.goal, task.description, user_message, ) if part ).lower() if not text.strip(): return False complex_markers = ( "agent team", "sub-agent", "multi-agent", "parallel", "dag", "workflow", "review", "research", "compare", "comparison", "architecture", "refactor", "multi-file", "end-to-end", "并行", "团队", "多智能体", "子代理", "工作流", "评审", "审查", "调研", "研究", "对比", "架构", "重构", "多文件", "端到端", ) return any(marker in text for marker in complex_markers)