242 lines
8.2 KiB
Python
242 lines
8.2 KiB
Python
"""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
|
|
]
|