"""Thin swarms orchestrator for `spawn_agent_team`.""" from __future__ import annotations from pathlib import Path from typing import Any from loguru import logger from nanobot.agent.agent_registry import AgentRegistry from nanobot.agent.process_events import emit_process_event from nanobot.agent_team.memory import ProcedureMemory, RunMemory from nanobot.agent_team.swarms_adapter import MemberRunner from nanobot.agent_team.swarms_bridge import SwarmsBridge from nanobot.agent_team.swarms_planner import SwarmsRunPlanner from nanobot.agent_team.swarms_policy import SwarmsPolicy from nanobot.agent_team.target_resolver import TargetResolver from nanobot.agent_team.types import BridgeResult, ExecutionMode from nanobot.providers.base import LLMProvider class AgentTeamOrchestrator: """Plan a swarms run, execute it, and persist the normalized result.""" def __init__( self, *, workspace: Path, provider: LLMProvider, model: str | None, registry: AgentRegistry, bus: Any, local_executor: Any, member_runner: MemberRunner, max_parallel_agents: int = 4, gateway_port: int = 18790, ) -> None: self.workspace = workspace self.registry = registry self.bus = bus self.local_executor = local_executor self.procedure_memory = ProcedureMemory(workspace) self.run_memory = RunMemory(workspace) self.policy = SwarmsPolicy(max_agents=max_parallel_agents) self.target_resolver = TargetResolver( workspace=workspace, registry=registry, provider=provider, model=model, max_parallel_agents=max_parallel_agents, gateway_port=gateway_port, ) self.planner = SwarmsRunPlanner( model=model, registry=registry, target_resolver=self.target_resolver, procedure_memory=self.procedure_memory, policy=self.policy, ) self.swarms = SwarmsBridge( workspace=workspace, registry=registry, member_runner=member_runner, ) @staticmethod def _clean_metadata(metadata: dict[str, Any]) -> dict[str, Any]: return { key: value for key, value in metadata.items() if value is not None and not (isinstance(value, str) and not value.strip()) and not (isinstance(value, (list, tuple, set, dict)) and not value) } async def _emit_trace( self, run_id: str, text: str, *, stage_label: str, metadata: dict[str, Any] | None = None, ) -> None: await emit_process_event( "process_run_progress", run_id=run_id, actor_type="system", actor_id="agent-team", actor_name="Agent Team", text=text, metadata=self._clean_metadata({ "source": "agent_team_orchestrator", "stage_label": stage_label, **(metadata or {}), }), ) async def run_task( self, *, task: str, label: str, skills: list[str], origin: dict[str, str], announce_via_bus: bool, run_id: str, ) -> BridgeResult: """Run the team task through swarms only.""" await self._emit_trace( run_id, "Preparing a swarms run specification for the agent team.", stage_label="准备 swarms 运行规格", metadata={ "phase": "planning", "skills": list(skills), "origin": dict(origin), "announce_via_bus": announce_via_bus, }, ) spec = await self.planner.plan(task=task, label=label, skills=list(skills)) await self._emit_trace( run_id, f"Swarms run spec is ready: {spec.swarm_type} with {len(spec.agent_ids)} agent(s).", stage_label="swarms 运行规格已就绪", metadata={ "phase": "planning", "spec": spec.to_dict(), }, ) logger.info( "Agent team [{}] running swarms type={} agents={}", run_id, spec.swarm_type, spec.agent_ids, ) cleanup: dict[str, Any] = {} try: result = await self.swarms.run_spec(spec=spec, run_id=run_id) finally: cleanup = await self._cleanup_created_specialists(spec, run_id) if cleanup: result.raw.setdefault("provisioning_cleanup", cleanup) if cleanup.get("created_targets"): # The run used temporary specialists that have now been removed; do not # persist a reusable procedure pointing at deleted agent ids. result.candidate_procedure = None result.raw.setdefault("origin", dict(origin)) result.raw.setdefault("announce_via_bus", announce_via_bus) stored_procedure = None if result.success: stored_procedure = await self.procedure_memory.record_candidate(task, result) await self.run_memory.record_run( task, ExecutionMode.SWARMS, result, procedure_id=( stored_procedure.id if stored_procedure is not None else ( result.matched_procedure.id if result.matched_procedure is not None else None ) ), ) await self._emit_trace( run_id, "Swarms agent team run completed.", stage_label="swarms 团队执行完成", metadata={ "phase": "completed", "success": result.success, "mode": result.mode.value, "stored_procedure_id": stored_procedure.id if stored_procedure else None, "attempt_count": len(result.attempts), }, ) return result async def _cleanup_created_specialists( self, spec: Any, run_id: str, ) -> dict[str, Any]: created_targets = self._created_provisioned_targets(spec) if not created_targets: return {} error = None try: deleted_targets = self.target_resolver.provisioning.cleanup_local_specialists(created_targets) except Exception as exc: deleted_targets = [] error = str(exc) logger.warning("Failed to clean up auto-provisioned agent-team specialists: {}", exc) deleted_set = set(deleted_targets) cleanup = { "created_targets": created_targets, "deleted_targets": deleted_targets, "skipped_targets": [ target for target in created_targets if target not in deleted_set ], } if error is not None: cleanup["error"] = error try: await self._emit_trace( run_id, "Cleaned up auto-provisioned agent-team specialists.", stage_label="清理自动创建的团队成员", metadata={ "phase": "cleanup", **cleanup, }, ) except Exception as exc: logger.warning("Failed to emit agent-team cleanup trace: {}", exc) return cleanup @staticmethod def _created_provisioned_targets(spec: Any) -> list[str]: metadata = getattr(spec, "metadata", {}) if not isinstance(metadata, dict): return [] target_plan = metadata.get("target_plan") if not isinstance(target_plan, dict): return [] created_targets = target_plan.get("created_provisioned_targets") if not created_targets: plan_metadata = target_plan.get("metadata") if isinstance(plan_metadata, dict): created_targets = plan_metadata.get("created_provisioned_targets") return [ target for target in dict.fromkeys(str(item).strip() for item in (created_targets or [])) if target ]