agent-Specialization/modules/sub_agent/creation.py
JOJO a1a9a47446 fix(multi-agent): 终结子智能体改为吸收态,修复终结后复活/停止无效
terminate_sub_agent 彻底化:
- 通过 _sub_agent_instances 找到活实例直接 cancel 其 _task(原实现只查
  _running_tasks,多 manager 并存时拿不到句柄导致 cancel 未送达)
- 实例置 _cancelled 并从注册表移除,防止 inject 找到活实例直接复活
- MultiAgentState 同步标记 terminated(原实现不写,侧边栏一直显示 idle)
- output.json 改写为 terminated 终态快照(原实现保留陈旧 idle 快照,
  会被 _check_task_status 读回复活任务记录)
- 新增 control.json 跨 manager 击杀/软停止信号通道,子智能体运行循环
  每个 idle tick / 每轮开始自行消费

注入/复活路径拒绝终结者:
- inject_message_to_sub_agent 检查最新任务记录,terminated 拒绝注入并
  清理孤儿实例;_revive_sub_agent 排除 terminated 记录
- send_message_to_sub_agent 工具返回明确的'已被终结'错误
- stop_sub_agent / soft_stop_all_agents 跳过 terminated,本地无实例时
  经 control.json 让对端 manager 上的实例自行软停止

terminated 吸收态防护:
- _check_task_status/_handle_running_snapshot 不再用陈旧快照覆盖终结记录
- output.json 为 terminated 时同步本地记录(跨 manager 终结广播)
- reconcile_task_states 的 idle/running 修正跳过终态任务
- MultiAgentState.mark_status 禁止 terminated 被覆盖回 idle/running
- 硬取消路径 output.json 写 terminated 而非 failed,避免 failed 被复活
2026-07-21 15:46:24 +08:00

97 lines
3.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""子智能体创建、查询与会话槽位管理工具。"""
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],
*,
include_idle: bool = False,
) -> Optional[Dict]:
self.reconcile_task_states()
if task_id:
return self.tasks.get(task_id)
if agent_id is None:
return None
# 多智能体模式下子智能体常驻 idle等待唤醒terminate/stop 需要能选中它们
allowed = {"pending", "running"}
if include_idle:
allowed = allowed | {"idle"}
candidates = [
task for task in self.tasks.values()
if task.get("agent_id") == agent_id and task.get("status") in allowed
]
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: Optional[str], *, multi_agent_mode: bool = False) -> 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 not multi_agent_mode and 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: Optional[str], *, multi_agent_mode: bool = False) -> Path:
relative_dir = (relative_dir or "").strip()
# 多智能体模式:没有交付目录概念,直接使用项目根目录
if multi_agent_mode and not relative_dir:
return self.project_path.resolve()
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