"""Agent 调度器 — 三级决策树 Level 1: 纯机械检查 → Daemon 本地执行 Level 2: 有名字的注册角色 → Full Agent (asyncio.create_subprocess_exec) Level 3: 无名字的一次性任务 → Subagent (sessions_spawn) Level 4: 未知 → 庞统裁决 调度器只做决策,实际 spawn 由 spawner.py 执行。 """ from __future__ import annotations import logging from enum import Enum from pathlib import Path from typing import Any, Dict, List, Optional from src.blackboard.models import Task from src.blackboard.queries import Queries logger = logging.getLogger("moziplus-v2.dispatcher") class DispatchLevel(str, Enum): """调度级别""" LOCAL = "local" # Daemon 本地执行 FULL_AGENT = "full" # Full Agent spawn SUB_AGENT = "sub" # Subagent spawn ESCALATE = "escalate" # 升级庞统 class Dispatcher: """Agent 调度决策器""" # L1 本地执行的 action type LOCAL_ACTIONS = frozenset({ "L1_guardrail", "format_check", "file_exists_check", "dependency_advance", }) def __init__( self, registered_agents: Optional[List[str]] = None, spawner: Optional[Any] = None, counter: Optional[Any] = None, ): """ Args: registered_agents: 项目注册的 Agent ID 列表 spawner: AgentSpawner 实例(执行实际 spawn) counter: ActiveAgentCounter 实例(控制并发) """ self.registered_agents = set(registered_agents or []) self.spawner = spawner self.counter = counter def decide(self, task: Task, action_type: str = "") -> Dict[str, Any]: """调度决策 Returns: {"level": DispatchLevel, "agent_id": str, "reason": str} """ assignee = task.assignee # Level 1: 纯机械检查 → 本地执行 if action_type in self.LOCAL_ACTIONS: return { "level": DispatchLevel.LOCAL, "agent_id": "daemon", "reason": f"Local action: {action_type}", } # Level 2: 有名字的注册角色 → Full Agent if assignee and assignee in self.registered_agents: new_session = action_type == "adjudication" return { "level": DispatchLevel.FULL_AGENT, "agent_id": assignee, "new_session": new_session, "reason": f"Registered agent: {assignee}", } # Level 3: 无名字或未注册 → Subagent if not assignee: return { "level": DispatchLevel.SUB_AGENT, "agent_id": "subagent", "reason": "No assignee, dispatch as subagent", } # Level 4: 有 assignee 但未注册 → 升级庞统 return { "level": DispatchLevel.ESCALATE, "agent_id": "pangtong-fujunshi", "new_session": True, "reason": f"Unknown agent '{assignee}', escalate to pangtong", } async def dispatch(self, task: Task, action_type: str = "", project_config: Optional[Dict] = None) -> Dict[str, Any]: """执行调度(决策 + spawn) Returns: {"level": str, "agent_id": str, "session_id": Optional[str], "status": "dispatched"|"skipped"|"error", "reason": str} """ decision = self.decide(task, action_type) level = decision["level"] agent_id = decision["agent_id"] # 检查并发限制 if self.counter and level in (DispatchLevel.FULL_AGENT, DispatchLevel.ESCALATE): if not await self.counter.can_acquire(agent_id): return { "level": level.value, "agent_id": agent_id, "session_id": None, "status": "skipped", "reason": "Agent busy (concurrent limit)", } # 本地执行 if level == DispatchLevel.LOCAL: return { "level": level.value, "agent_id": "daemon", "session_id": None, "status": "dispatched", "reason": decision["reason"], } # Full Agent spawn if level in (DispatchLevel.FULL_AGENT, DispatchLevel.ESCALATE): if not self.spawner: return { "level": level.value, "agent_id": agent_id, "session_id": None, "status": "error", "reason": "No spawner configured", } try: if self.counter: await self.counter.acquire(agent_id) session_id = await self.spawner.spawn_full_agent( agent_id=agent_id, message=self._build_message(task, action_type), new_session=decision.get("new_session", False), task_id=task.id, ) return { "level": level.value, "agent_id": agent_id, "session_id": session_id, "status": "dispatched", "reason": decision["reason"], } except Exception as e: if self.counter: self.counter.release(agent_id) return { "level": level.value, "agent_id": agent_id, "session_id": None, "status": "error", "reason": str(e), } # Subagent spawn if level == DispatchLevel.SUB_AGENT: if not self.spawner: return { "level": level.value, "agent_id": "subagent", "session_id": None, "status": "error", "reason": "No spawner configured", } try: session_id = await self.spawner.spawn_subagent( task_description=self._build_message(task, action_type), task_id=task.id, ) return { "level": level.value, "agent_id": "subagent", "session_id": session_id, "status": "dispatched", "reason": decision["reason"], } except Exception as e: return { "level": level.value, "agent_id": "subagent", "session_id": None, "status": "error", "reason": str(e), } return { "level": level.value, "agent_id": agent_id, "session_id": None, "status": "error", "reason": "Unknown dispatch level", } def _build_message(self, task: Task, action_type: str) -> str: """构建给 Agent 的消息""" parts = [f"Task: {task.title}"] if task.description: parts.append(f"Description: {task.description}") if action_type: parts.append(f"Action: {action_type}") if task.must_haves: import json parts.append(f"Must-haves: {task.must_haves}") return "\n".join(parts) def dispatch_pending(self, tasks: List[Task]) -> List[Dict[str, Any]]: """批量决策(不 spawn,只返回决策列表) 用于 ticker 扫描可调度任务后批量判断。 """ results = [] for task in tasks: decision = self.decide(task) results.append({ "task_id": task.id, **decision, }) return results