"""Bridge from nanobot agent-team tasks into the vendored swarms runtime.""" from __future__ import annotations import asyncio import json from pathlib import Path from typing import Any from nanobot.agent.agent_registry import AgentRegistry from nanobot.agent.process_events import emit_process_event from nanobot.agent.run_result import has_meaningful_summary from nanobot.agent_team.swarms_adapter import MemberRunner, NanobotAgentAdapter, load_swarms_runtime from nanobot.agent_team.types import ( BridgeAttempt, BridgeResult, ExecutionMode, ProcedureRecord, SwarmsRunResult, SwarmsRunSpec, ) class SwarmsBridge: """Execute a `SwarmsRunSpec` with `SwarmRouter` and normalize the output.""" def __init__( self, *, workspace: Path, registry: AgentRegistry, member_runner: MemberRunner, ) -> None: self.workspace = workspace self.registry = registry self.member_runner = member_runner async def run_spec(self, *, spec: SwarmsRunSpec, run_id: str) -> BridgeResult: # 先发一条过程事件,告诉上层“swarms 执行阶段已经开始”。 # metadata 里带完整 spec,便于前端或日志侧排查本次实际执行参数。 await self._emit_progress( run_id, f"Starting swarms run: {spec.swarm_type}.", stage_label="启动 swarms runtime", metadata={"spec": spec.to_dict()}, ) # 真正调用 swarms runtime,返回的是“桥接层内部使用”的 SwarmsRunResult。 swarms_result = await self._run_swarms(spec=spec, run_id=run_id) # success 不只看 swarms_result.success,还要求 summary 有实际内容。 # 这样可以避免 runtime technically 跑完了,但最终没有任何可消费结论时, # 上层误把它当成一次成功执行。 success = swarms_result.success and has_meaningful_summary(swarms_result.summary) error = None if success else (swarms_result.error or swarms_result.summary) # BridgeAttempt 表示“这次 swarms 模式尝试”的完整快照; # 后续 BridgeResult.attempts 可以累计不同执行策略/回退路径的尝试记录。 attempt = BridgeAttempt( mode=ExecutionMode.SWARMS, success=success, summary=swarms_result.summary, error=error, member_results=list(swarms_result.member_results), targets=list(spec.agent_ids), raw={ "spec": spec.to_dict(), "swarms_result": swarms_result.to_dict(), }, ) # 只有成功时才生成 candidate procedure,避免把失败或空结果学习成可复用流程。 candidate = self._candidate_procedure(spec, swarms_result, run_id) if success else None # 再发一条归一化完成事件,让编排层知道 bridge 已经把 swarms 原始输出 # 压成了 nanobot 可消费的标准结果结构。 await self._emit_progress( run_id, "Swarms run returned a normalized bridge result.", stage_label="swarms 输出已归一", metadata={ "success": success, "swarm_type": spec.swarm_type, "candidate_procedure_id": candidate.id if candidate else None, }, ) # BridgeResult 是 swarms bridge 对外暴露的稳定边界: # - summary/member_results 给上层公告和持久化使用 # - attempts/raw 保留足够多细节,便于后续解释和调试 return BridgeResult( mode=ExecutionMode.SWARMS, success=success, summary=swarms_result.summary, error=error, member_results=list(swarms_result.member_results), candidate_procedure=candidate, attempts=[attempt], raw={ "spec": spec.to_dict(), "swarms_result": swarms_result.to_dict(), }, ) async def _run_swarms(self, *, spec: SwarmsRunSpec, run_id: str) -> SwarmsRunResult: try: # 先把 spec.agent_ids 解析成当前 registry 中的 AgentDescriptor。 # 这里显式校验 agent 必须存在,避免 swarms runtime 在更深处才报模糊错误。 descriptors = [] for agent_id in spec.agent_ids: descriptor = self.registry.get_agent(agent_id) if descriptor is None: raise ValueError(f"Agent not found for swarms run: {agent_id}") descriptors.append(descriptor) # swarms runtime 运行在线程池里,但每个 NanobotAgentAdapter 最终仍要把执行 # 切回当前事件循环中的 member_runner,因此这里提前拿到 running loop。 loop = asyncio.get_running_loop() # 把 nanobot 的 AgentDescriptor 包装成 swarms 可以直接调用的 adapter。 # swarms 视角下它们只是“可调用 agent”;nanobot 视角下它们会回流到 # member_runner,再由本地执行器或 A2A client 真正完成任务。 adapters = [ NanobotAgentAdapter( descriptor=descriptor, run_id=run_id, loop=loop, member_runner=self.member_runner, skills=list(spec.skills), ) for descriptor in descriptors ] # SwarmRouter 是 vendored swarms runtime 的核心入口。 # 这里把 planner 产出的 swarm_type / loops / flow / rules 全部映射进去。 runtime = load_swarms_runtime() router = runtime["SwarmRouter"]( name=spec.label or "nanobot-agent-team", description="Nanobot agent-team swarms router", agents=adapters, swarm_type=spec.swarm_type, max_loops=max(1, spec.max_loops), rearrange_flow=spec.rearrange_flow, rules=self._rules_with_skills(spec), autosave=False, verbose=False, ) # swarms 的 router.run 是同步阻塞调用,因此放到线程池中执行, # 避免阻塞当前 asyncio 事件循环。 raw_output = await asyncio.to_thread(router.run, task=self._task_with_skills(spec)) # swarms 原始输出结构并不稳定,统一在这里归一成 SwarmsRunResult。 return self._normalize_swarms_output(raw_output, adapters) except Exception as exc: # 桥接层把异常收口成失败结果,而不是继续向上抛, # 这样 orchestrator 可以用统一的 BridgeResult 流程处理失败。 return SwarmsRunResult( success=False, summary=f"Swarms execution failed: {exc}", raw_output=None, error=str(exc), ) def _rules_with_skills(self, spec: SwarmsRunSpec) -> str: # 把上层规则和桥接层的硬约束拼到一起: # 1. 保留 planner 指定的 rules # 2. 明确禁止 swarms 擅自引入额外 agent、工具或凭证 # 3. 把 skills 也写入规则,确保团队行为不偏离 nanobot 约束 parts = [ spec.rules or "Run the nanobot agent team through swarms and produce a concise synthesis.", "Do not add tools, credentials, network endpoints, or agents outside the provided nanobot adapters.", ] if spec.skills: parts.append("Required nanobot skills: " + ", ".join(spec.skills)) return "\n".join(parts) def _task_with_skills(self, spec: SwarmsRunSpec) -> str: # skills 既体现在 rules 中,也直接拼到任务文本里, # 这样无论 swarms runtime 更依赖哪部分上下文,都能看到技能约束。 if not spec.skills: return spec.task return ( f"{spec.task}\n\n" "Required skills for this swarms run:\n" f"{', '.join(spec.skills)}" ).strip() def _normalize_swarms_output( self, raw_output: Any, adapters: list[NanobotAgentAdapter], ) -> SwarmsRunResult: # 优先从 adapters 收集每个成员真实执行后的 AgentRunResult。 # 这些结果比 swarms runtime 的自由格式输出更稳定、也更适合后续持久化。 member_results = [ result for adapter in adapters for result in adapter.results ] # summary 优先从成员结果推导;如果成员结果拿不到,再从 swarms 原始输出中兜底提取。 summary = self._summary_from_swarms_output(raw_output, member_results) return SwarmsRunResult( success=bool(summary.strip()), summary=summary.strip(), raw_output=self._jsonable(raw_output), member_results=member_results, ) def _summary_from_swarms_output(self, raw_output: Any, member_results: list[Any]) -> str: # 如果已经拿到了结构化 member_results,就优先用它们生成总结, # 因为这比直接依赖 swarms 的原始输出更稳定、更贴近 nanobot 的结果模型。 if member_results: return "\n\n".join( f"{result.agent_name} ({result.status}):\n{result.summary}" for result in member_results if str(result.summary or "").strip() ) # swarms 有时直接返回字符串,那就把它当作最终 summary。 if isinstance(raw_output, str): return raw_output.strip() # swarms 也可能返回 transcript/list 结构;这里尝试提取非 user/system 的发言, # 拼成一个可读摘要。 if isinstance(raw_output, list): lines: list[str] = [] for item in raw_output: if not isinstance(item, dict): continue role = str(item.get("role") or item.get("speaker") or "").strip() content = str(item.get("content") or item.get("message") or "").strip() if not content or role.lower() in {"user", "system"}: continue lines.append(f"{role}: {content}" if role else content) if lines: return "\n\n".join(lines) # 最后兜底把原始输出尽量序列化成 JSON 文本;再不行就直接 str(...)。 try: return json.dumps(raw_output, ensure_ascii=False, indent=2) except TypeError: return str(raw_output) def _jsonable(self, value: Any) -> Any: # raw_output 最终要落到 BridgeResult / RunMemory 里,因此这里尽量保证它可序列化。 # 若原值无法直接 JSON 化,则退回字符串表示,避免整个持久化流程失败。 try: json.dumps(value, ensure_ascii=False) return value except TypeError: return str(value) def _candidate_procedure( self, spec: SwarmsRunSpec, result: SwarmsRunResult, run_id: str, ) -> ProcedureRecord: # bridge 只负责产出一个“可候选复用”的 procedure 草稿: # - task_template/agent_ids/strategy 记录执行骨架 # - summary 提供人类可读概览 # - metadata 记录它来自 swarms bridge # 真正是否持久化、如何更新统计,由更上层的 procedure memory 决定。 return ProcedureRecord( task_template=spec.task, summary=result.summary, agent_ids=list(spec.agent_ids), strategy=spec.swarm_type, confidence=0.6, source_run_id=run_id, metadata={ "source": "swarms_bridge", "swarm_type": spec.swarm_type, "auto_generated": spec.auto_generated, "skills": list(spec.skills), }, ) async def _emit_progress( self, run_id: str, text: str, *, stage_label: str, metadata: dict[str, Any] | None = None, ) -> None: # 统一发 process_run_progress,让前端/日志看到 swarms bridge 当前阶段。 await emit_process_event( "process_run_progress", run_id=run_id, actor_type="system", actor_id="swarms-bridge", actor_name="Swarms Bridge", text=text, metadata={ "source": "swarms_bridge", "stage_label": stage_label, **(metadata or {}), }, )