- 重写子智能体执行核心,不再启动 easyagent Node.js 子进程 - 新增 modules/sub_agent/ 包集中管理子智能体逻辑 - 工具调用复用主进程 WebTerminal.handle_tool_call,自然经过沙箱/容器链路 - 子智能体模型独立读取 ~/.agents/<mode>/config/sub_agent_models.json - 支持 8 个工具:read_file/write_file/edit_file(replacements+replace_all)/run_command/web_search/extract_webpage/search_workspace/read_mediafile - 修复子智能体进度弹窗:标题颜色、write_file 显示、过滤非 progress 条目、统一滚动条样式 - 更新 AGENTS.md / CLAUDE.md 子智能体描述 - 新增 test/test_sub_agent_regression.py 回归测试
83 lines
3.3 KiB
Python
83 lines
3.3 KiB
Python
"""子智能体创建、查询与会话槽位管理工具。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
import uuid
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from config import SUB_AGENT_DEFAULT_TIMEOUT, SUB_AGENT_MAX_ACTIVE
|
|
|
|
|
|
class SubAgentCreationMixin:
|
|
"""提供子智能体创建参数校验、任务ID生成、交付目录解析与槽位管理能力。"""
|
|
|
|
tasks: Dict[str, Dict[str, Any]]
|
|
conversation_agents: Dict[str, List[int]]
|
|
project_path: Path
|
|
|
|
def _select_task(self, task_id: Optional[str], agent_id: Optional[int]) -> Optional[Dict]:
|
|
self.reconcile_task_states()
|
|
if task_id:
|
|
return self.tasks.get(task_id)
|
|
if agent_id is None:
|
|
return None
|
|
candidates = [
|
|
task for task in self.tasks.values()
|
|
if task.get("agent_id") == agent_id and task.get("status") in {"pending", "running"}
|
|
]
|
|
if candidates:
|
|
candidates.sort(key=lambda item: item.get("created_at", 0), reverse=True)
|
|
return candidates[0]
|
|
return None
|
|
|
|
def _active_task_count(self, conversation_id: Optional[str] = None) -> int:
|
|
self.reconcile_task_states(conversation_id=conversation_id)
|
|
active = [t for t in self.tasks.values() if t.get("status") in {"pending", "running"}]
|
|
if conversation_id:
|
|
active = [t for t in active if t.get("conversation_id") == conversation_id]
|
|
return len(active)
|
|
|
|
def _ensure_agent_slot_available(self, conversation_id: str, agent_id: int) -> bool:
|
|
used = self.conversation_agents.setdefault(conversation_id, [])
|
|
return agent_id not in used
|
|
|
|
def _mark_agent_id_used(self, conversation_id: str, agent_id: int):
|
|
used = self.conversation_agents.setdefault(conversation_id, [])
|
|
if agent_id not in used:
|
|
used.append(agent_id)
|
|
|
|
def _validate_create_params(self, agent_id: Optional[int], summary: str, task: str, target_dir: str) -> Optional[str]:
|
|
if agent_id is None:
|
|
return "子智能体代号不能为空"
|
|
try:
|
|
agent_id = int(agent_id)
|
|
except ValueError:
|
|
return "子智能体代号必须是整数"
|
|
if agent_id <= 0:
|
|
return "子智能体代号必须为正整数"
|
|
if not summary or not summary.strip():
|
|
return "任务摘要不能为空"
|
|
if not task or not task.strip():
|
|
return "任务详情不能为空"
|
|
if target_dir is None:
|
|
return "指定文件夹不能为空"
|
|
return None
|
|
|
|
def _generate_task_id(self, agent_id: int) -> str:
|
|
suffix = uuid.uuid4().hex[:6]
|
|
return f"sub_{agent_id}_{int(time.time())}_{suffix}"
|
|
|
|
def _resolve_deliverables_dir(self, relative_dir: str) -> Path:
|
|
relative_dir = relative_dir.strip() if relative_dir else ""
|
|
if not relative_dir:
|
|
raise ValueError("交付目录不能为空,必须指定")
|
|
deliverables_path = (self.project_path / relative_dir).resolve()
|
|
if not str(deliverables_path).startswith(str(self.project_path)):
|
|
raise ValueError("交付目录必须位于项目目录内")
|
|
if deliverables_path.exists():
|
|
raise ValueError("交付目录必须为不存在的新目录")
|
|
deliverables_path.mkdir(parents=True, exist_ok=True)
|
|
return deliverables_path
|