修改了nanobot,往Hermes agent的风格走,进度1/3

This commit is contained in:
2026-04-20 18:11:14 +08:00
parent cdfc222c9f
commit 36882a7d7b
261 changed files with 12659 additions and 604 deletions

View File

@ -0,0 +1,63 @@
"""Agent Team swarms adapter package."""
from __future__ import annotations
from importlib import import_module
from typing import Any
__all__ = [
"AgentTeamOrchestrator",
"BridgeAttempt",
"BridgeResult",
"ExecutionMode",
"NanobotAgentAdapter",
"ProcedureMemory",
"ProcedureRecord",
"ResolvedTeamPlan",
"RunMemory",
"RunRecord",
"SwarmsBridge",
"SwarmsPolicy",
"SwarmsRunPlanner",
"SwarmsRunResult",
"SwarmsRunSpec",
]
def __getattr__(name: str) -> Any:
if name == "AgentTeamOrchestrator":
from nanobot.agent_team.orchestrator import AgentTeamOrchestrator
return AgentTeamOrchestrator
if name == "NanobotAgentAdapter":
from nanobot.agent_team.swarms_adapter import NanobotAgentAdapter
return NanobotAgentAdapter
if name == "SwarmsBridge":
from nanobot.agent_team.swarms_bridge import SwarmsBridge
return SwarmsBridge
if name == "SwarmsPolicy":
from nanobot.agent_team.swarms_policy import SwarmsPolicy
return SwarmsPolicy
if name == "SwarmsRunPlanner":
from nanobot.agent_team.swarms_planner import SwarmsRunPlanner
return SwarmsRunPlanner
if name in {"ProcedureMemory", "RunMemory"}:
memory = import_module("nanobot.agent_team.memory")
return getattr(memory, name)
if name in {
"BridgeAttempt",
"BridgeResult",
"ExecutionMode",
"ProcedureRecord",
"ResolvedTeamPlan",
"RunRecord",
"SwarmsRunResult",
"SwarmsRunSpec",
}:
types = import_module("nanobot.agent_team.types")
return getattr(types, name)
raise AttributeError(name)

View File

@ -0,0 +1,361 @@
"""Agent Team 的轻量持久化层。
这里没有引入数据库,
而是参考轻量 file store 设计:
1. 数据结构尽量稳定;
2. 使用原子写覆盖,避免半写状态;
3. 单文件规模保持小而可读,便于排查与测试。
"""
from __future__ import annotations
import json
import os
import re
from pathlib import Path
from typing import Any
from nanobot.agent.run_result import contains_placeholder_summary, has_meaningful_summary
from nanobot.agent_team.types import (
BridgeResult,
ExecutionMode,
ProcedureRecord,
RunRecord,
now_iso,
)
# ASCII token 用于英文/agent id/命令片段匹配。
_ASCII_TOKEN_RE = re.compile(r"[a-z0-9_:-]+")
# 中文任务没有自然空格,这里退而求其次按单字切分,保证最小可匹配能力。
_CJK_CHAR_RE = re.compile(r"[\u4e00-\u9fff]")
def _memory_root(workspace: Path) -> Path:
"""返回 agent team memory 根目录。
Demo 输出:
`/workspace/agent_team`
"""
# 独立目录便于用户直接查看 procedure/runs 文件,不和其他 runtime 状态混在一起。
root = workspace / "agent_team"
root.mkdir(parents=True, exist_ok=True)
return root
def _load_json(path: Path, default: Any) -> Any:
"""从磁盘加载 JSON损坏或不存在时回退到默认值。
Demo 输出:
`[]`
"""
# agent team memory 不应因为单个文件损坏就拖垮主链路,所以统一做软失败。
if not path.exists():
return default
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError, json.JSONDecodeError):
return default
def _atomic_write_json(path: Path, payload: Any) -> None:
"""把 JSON 原子写入目标路径。
Demo 输出:
`None`
"""
# 先写临时文件再 `os.replace`,这样即使进程中断也不会留下半截 JSON。
path.parent.mkdir(parents=True, exist_ok=True)
tmp_path = path.with_suffix(path.suffix + ".tmp")
tmp_path.write_text(
json.dumps(payload, indent=2, ensure_ascii=False),
encoding="utf-8",
)
os.replace(str(tmp_path), str(path))
def task_tokens(text: str) -> list[str]:
"""把任务文本压成可匹配的轻量 token 列表。
Demo 输出:
`["生成", "周报", "writer-agent", "publish"]`
"""
# 统一小写,保证 agent id、英文命令和 task keywords 比较时大小写无关。
lowered = (text or "").strip().lower()
if not lowered:
return []
# 英文 token 适合匹配 agent id、命令词和常见英文任务描述。
ascii_tokens = [token for token in _ASCII_TOKEN_RE.findall(lowered) if len(token) > 1]
# 中文这里按单字匹配,虽然粗糙,但比整句更利于无分词依赖的第一版实现。
cjk_tokens = _CJK_CHAR_RE.findall(lowered)
# 用 `dict.fromkeys` 去重并保持原始顺序,便于后续测试断言更稳定。
return list(dict.fromkeys([*ascii_tokens, *cjk_tokens]))
def similarity_score(query_tokens: list[str], candidate_tokens: list[str]) -> float:
"""按 token 重叠度计算相似度。
Demo 输出:
`0.67`
"""
# 任一侧为空都说明没有稳定的匹配依据,直接给 0。
if not query_tokens or not candidate_tokens:
return 0.0
# 这里故意不做复杂权重,保持算法透明、可预测、可测试。
query_set = set(query_tokens)
candidate_set = set(candidate_tokens)
overlap = len(query_set & candidate_set)
if overlap <= 0:
return 0.0
# 使用 `max(len(query), len(candidate))` 作为分母,让长任务模板不会被短查询轻易误命中。
return overlap / max(len(query_set), len(candidate_set))
def clip_confidence(value: float) -> float:
"""把置信度裁剪到 `[0.0, 1.0]`。
Demo 输出:
`0.8`
"""
# 所有 confidence 更新都收口到这里,避免散落的边界处理不一致。
return max(0.0, min(1.0, round(value, 4)))
class ProcedureMemory:
"""管理 learned procedure 的持久化和匹配。
公开方法都带了 Demo 输出说明,便于用户直接对照磁盘结果和测试脚本理解行为。
"""
def __init__(
self,
workspace: Path,
*,
min_confidence: float = 0.55,
match_threshold: float = 0.2,
) -> None:
"""初始化 procedure memory。
Demo 输出:
`ProcedureMemory(workspace=/tmp/demo-workspace, procedures.json ready)`
"""
# `procedures.json` 用数组存储,人工排查时最直观。
self.workspace = workspace
self.path = _memory_root(workspace) / "procedures.json"
# 低于该值的 procedure 即使匹配到关键词,也不建议作为复用提示。
self.min_confidence = min_confidence
# 匹配阈值保持较低,只作为 AutoSwarmBuilder / planner 的参考提示。
self.match_threshold = match_threshold
def list_procedures(self) -> list[ProcedureRecord]:
"""读取全部 procedure 记录并按置信度排序。
Demo 输出:
`[ProcedureRecord(...), ProcedureRecord(...)]`
"""
# 文件损坏或不存在时直接回空列表,主流程会自动退回探索模式。
raw = _load_json(self.path, [])
records = [
ProcedureRecord.from_dict(item)
for item in raw
if isinstance(item, dict)
]
# 高置信度、最近更新的记录更靠前,方便测试和人工查看。
records.sort(key=lambda item: (item.confidence, item.updated_at), reverse=True)
return records
def match_procedure(self, task: str) -> ProcedureRecord | None:
"""为当前任务匹配最合适的 procedure。
Demo 输出:
`ProcedureRecord(id='procedure-a1b2c3d4', task_template='生成周报', ...)`
"""
# 没有 token 说明任务文本几乎为空,此时不应命中任何 procedure。
query_tokens = task_tokens(task)
if not query_tokens:
return None
best_record: ProcedureRecord | None = None
best_score = 0.0
for record in self.list_procedures():
# 明显是占位/空结果的历史 procedure 直接忽略,避免污染后续路由。
if contains_placeholder_summary(record.summary):
continue
# 优先用关键词匹配;任务模板是人工兜底线索。
candidate_tokens = record.task_keywords or task_tokens(record.task_template)
score = similarity_score(query_tokens, candidate_tokens)
# task_template 全量包含时,给一个小额加分,提高近似重跑命中率。
if record.task_template and record.task_template.lower() in task.lower():
score += 0.1
# 最终排序同时考虑相似度、置信度和失败率,避免高失败 procedure 反复被选中。
weighted = score + record.confidence * 0.2 - record.failure_rate() * 0.2
if weighted > best_score:
best_record = record
best_score = weighted
# 分数不足则视为没有可靠命中,让上层走探索式执行。
if best_record is None or best_score < self.match_threshold:
return None
return best_record
async def record_candidate(self, task: str, result: BridgeResult) -> ProcedureRecord | None:
"""把探索阶段产出的候选 procedure 写入 memory。
Demo 输出:
`ProcedureRecord(id='procedure-a1b2c3d4', confidence=0.6, success_count=1, ...)`
"""
# 只有 bridge 显式产出候选 procedure 时才会落盘。
candidate = result.candidate_procedure
if candidate is None:
return None
if not has_meaningful_summary(candidate.summary):
return None
# 记录写入时间统一在这里刷新,保证磁盘上的排序行为可预测。
timestamp = now_iso()
# 任务 token 统一在持久化层补齐,保证不依赖具体 bridge 的实现细节。
merged_keywords = list(dict.fromkeys([*candidate.task_keywords, *task_tokens(task)]))
candidate.task_keywords = merged_keywords
candidate.task_template = candidate.task_template or task
candidate.summary = candidate.summary or result.summary
candidate.confidence = clip_confidence(candidate.confidence or 0.55)
candidate.created_at = candidate.created_at or timestamp
candidate.updated_at = timestamp
records = self.list_procedures()
best_index: int | None = None
best_score = 0.0
for index, record in enumerate(records):
# 完全相同 agent 组合视为强相关;否则退回关键词重叠比对。
same_agents = (
record.strategy == candidate.strategy
and record.agent_ids == candidate.agent_ids
)
score = 1.0 if same_agents else similarity_score(candidate.task_keywords, record.task_keywords)
if score > best_score:
best_index = index
best_score = score
if best_index is not None and best_score >= 0.5:
# 合并已有记录,避免每次探索都生成一条几乎重复的 procedure。
current = records[best_index]
current.task_template = candidate.task_template or current.task_template
current.summary = candidate.summary or current.summary
current.agent_ids = list(candidate.agent_ids) or current.agent_ids
current.strategy = candidate.strategy or current.strategy
current.task_keywords = list(dict.fromkeys([*current.task_keywords, *candidate.task_keywords]))
current.confidence = clip_confidence(max(current.confidence, candidate.confidence))
current.success_count += 1
current.updated_at = timestamp
current.metadata.update(candidate.metadata)
current.source_run_id = candidate.source_run_id or current.source_run_id
stored = current
else:
# 新候选第一次入库时直接记为一次成功学习。
candidate.success_count = max(candidate.success_count, 1)
candidate.failure_count = max(candidate.failure_count, 0)
candidate.created_at = candidate.created_at or timestamp
candidate.updated_at = timestamp
records.append(candidate)
stored = candidate
_atomic_write_json(self.path, [item.to_dict() for item in records])
return stored
async def update_confidence(self, procedure_id: str, delta: float) -> ProcedureRecord | None:
"""更新某条 procedure 的置信度与成败计数。
Demo 输出:
`ProcedureRecord(id='procedure-a1b2c3d4', confidence=0.75, success_count=2, failure_count=0, ...)`
"""
# 没有主键时直接回空,避免误更新所有记录。
if not procedure_id:
return None
records = self.list_procedures()
updated: ProcedureRecord | None = None
for record in records:
if record.id != procedure_id:
continue
# 所有状态变更都集中在这里,保证计数和 confidence 始终同步。
record.confidence = clip_confidence(record.confidence + delta)
# 统一刷新“最近一次使用”和“最近一次更新时间”,这两个字段都服务于路由与排障。
timestamp = now_iso()
record.updated_at = timestamp
record.last_used_at = timestamp
if delta >= 0:
record.success_count += 1
else:
record.failure_count += 1
updated = record
break
if updated is None:
return None
_atomic_write_json(self.path, [item.to_dict() for item in records])
return updated
class RunMemory:
"""管理 run 级别的历史记录。"""
def __init__(self, workspace: Path, *, max_records: int = 200) -> None:
"""初始化 run memory。
Demo 输出:
`RunMemory(workspace=/tmp/demo-workspace, runs.json ready)`
"""
# `runs.json` 保持轻量滚动窗口,避免长期运行后无限膨胀。
self.workspace = workspace
self.path = _memory_root(workspace) / "runs.json"
self.max_records = max(1, max_records)
def list_runs(self) -> list[RunRecord]:
"""读取全部 run 记录。
Demo 输出:
`[RunRecord(...), RunRecord(...)]`
"""
raw = _load_json(self.path, [])
return [
RunRecord.from_dict(item)
for item in raw
if isinstance(item, dict)
]
async def record_run(
self,
task: str,
mode: ExecutionMode,
result: BridgeResult,
procedure_id: str | None = None,
) -> RunRecord:
"""把一次 agent team 运行结果落盘。
Demo 输出:
`RunRecord(id='run-1a2b3c4d', mode=<ExecutionMode.SWARMS: 'swarms'>, success=True, ...)`
"""
# 把 attempt/原始 bridge 结果也带进 metadata后面排查 swarms 执行很有用。
record = RunRecord(
task=task,
mode=mode,
success=result.success,
summary=result.summary,
error=result.error,
procedure_id=procedure_id or (result.matched_procedure.id if result.matched_procedure else None),
metadata={
"attempts": [attempt.to_dict() for attempt in result.attempts],
"bridge_result": result.to_dict(),
},
)
runs = self.list_runs()
runs.append(record)
# 只保留最近 N 条,保证 JSON 文件体积可控。
if len(runs) > self.max_records:
runs = runs[-self.max_records:]
_atomic_write_json(self.path, [item.to_dict() for item in runs])
return record

View File

@ -0,0 +1,241 @@
"""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
]

View File

@ -0,0 +1,185 @@
"""Provision managed local A2A specialists for agent teams."""
from __future__ import annotations
import hashlib
import os
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from loguru import logger
from nanobot.agent.subagents import LocalSubagentStore, normalize_subagent_id
from nanobot.config.schema import Config
@dataclass(frozen=True)
class SpecialistProvisionResult:
"""Result of ensuring a managed specialist exists."""
agent_id: str
created: bool
class ProvisioningManager:
"""Manage local specialists through LocalSubagentStore."""
def __init__(self, workspace: Path, *, gateway_port: int = 18790) -> None:
self.workspace = workspace
self.gateway_port = int(os.getenv("APP_BACKEND_PORT") or gateway_port)
self.store = LocalSubagentStore(workspace)
async def ensure_local_specialist_with_result(
self,
*,
role: str,
task: str,
skills: list[str] | None = None,
) -> SpecialistProvisionResult:
"""创建或刷新一个本地 specialist并返回它是否是首次创建。"""
# role 可能来自上游 planner、用户输入或其他动态流程这里先做兜底和规范化
# 1. 空值时退回到通用角色 "general specialist"
# 2. 去掉首尾空白,避免生成不稳定的 agent 标识
# 这样可以保证后续 id、显示名、标签等字段都基于同一个干净的角色名生成。
role_name = str(role or "general specialist").strip() or "general specialist"
# agent_id 由“角色名 + 任务指纹”组成:
# - 同一角色处理同一任务时会命中同一个 id从而实现刷新/复用
# - 同一角色处理不同任务时会得到不同 id避免不同任务上下文互相污染
agent_id = self._specialist_id(role_name, task)
# display_name 主要用于人类可读展示;它不影响真正的唯一性,
# 唯一性仍由 agent_id 保证。
display_name = self._display_name(role_name)
# 为即将 upsert 的 subagent 构造运行时配置。
# 这里显式覆盖两个关键字段:
# - workspace确保 specialist 和当前 agent team 运行在同一个工作目录
# - gateway.port确保它连接到当前后端实例暴露的网关端口
# 这样新建/刷新出来的本地 specialist 才能在正确的环境里工作。
config = Config()
config.agents.defaults.workspace = str(self.workspace)
config.gateway.port = self.gateway_port
# payload 是写入 LocalSubagentStore 的完整声明式规格。
# store.upsert_subagent(...) 会根据这份规格创建或刷新 subagent。
payload = {
# 稳定唯一 id用于判断“是否已存在”以及后续更新同一个 specialist。
"id": agent_id,
# 人类可读名称,便于在 UI、日志或调试信息中识别角色。
"name": display_name,
# 简短描述说明该 agent 的来源和用途:它是 agent team 自动托管的本地 A2A specialist。
"description": f"Managed local A2A specialist for {role_name}.",
# system_prompt 注入角色视角、原始任务以及本次要求携带的技能上下文,
# 是 specialist 实际行为边界和任务目标的核心输入。
"system_prompt": self._system_prompt(role_name, task, skills or []),
# 允许它进行完整委派;也就是说该 specialist 自己可以继续向下分派任务,
# 而不是被限制为只能本地直接回答。
"delegation_mode": "full",
# 允许访问 MCP表示这个 specialist 在受外层权限控制的前提下可以使用 MCP 能力。
"allow_mcp": True,
# tags 用于分类、筛选和后续清理:
# - auto-provisioned / agent-team标明它是系统自动创建的团队成员
# - role_name.replace(" ", "-"):保留一个角色维度标签,便于检索
# - skills把本次技能要求也落到标签中方便观测和调试
# 使用 set 去重、sorted 排序,保证结果稳定。
"tags": sorted(set(["auto-provisioned", "agent-team", role_name.replace(" ", "-")] + list(skills or []))),
# aliases 提供额外可匹配名称,既支持原始角色名,也支持格式化后的展示名。
"aliases": [role_name, display_name],
# metadata 存放程序消费的结构化信息:
# - managed_by标记由哪个模块托管后续 cleanup 时会用来判定是否允许删除
# - role记录规范化后的角色名
# - task_fingerprint记录任务指纹便于追踪这个 specialist 绑定的是哪类任务上下文
"metadata": {
"managed_by": "agent_team_provisioning",
"role": role_name,
"task_fingerprint": self._fingerprint(task),
},
}
# 先读取一次已有记录,用于区分“首次创建”还是“刷新已有 specialist”。
# 注意:真正的写入动作由后面的 upsert 完成。
existing = self.store.get_subagent(agent_id)
# upsert 语义是:
# - 不存在则创建
# - 已存在则按新的 payload/config 刷新
# 这样调用方不需要区分 create / update 两条路径。
spec = self.store.upsert_subagent(payload, config)
# 日志区分 provisioned 和 refreshed便于排查
# - 为什么这次新建了一个 specialist
# - 或者为什么只是把旧的配置重新覆盖了一次
if existing is None:
logger.info("Provisioned local A2A specialist {} for role '{}'", spec.id, role_name)
else:
logger.info("Refreshed local A2A specialist {} for role '{}'", spec.id, role_name)
# 返回两类关键信息:
# - agent_id供上游继续引用这个 specialist
# - created明确告知这次是首次创建还是命中了已有对象并完成刷新
return SpecialistProvisionResult(agent_id=spec.id, created=existing is None)
def cleanup_local_specialists(self, agent_ids: list[str]) -> list[str]:
"""Delete managed specialists and return the ids actually removed."""
deleted: list[str] = []
for agent_id in dict.fromkeys(str(item).strip() for item in agent_ids if str(item).strip()):
spec = self.store.get_subagent(agent_id)
if spec is None:
continue
if not self._is_managed_specialist(spec.metadata, spec.tags):
logger.warning("Skipping cleanup for unmanaged local specialist candidate {}", agent_id)
continue
if self.store.delete_subagent(agent_id):
deleted.append(agent_id)
logger.info("Cleaned up local A2A specialist {}", agent_id)
return deleted
@staticmethod
def _is_managed_specialist(metadata: dict[str, Any], tags: list[str]) -> bool:
return (
metadata.get("managed_by") == "agent_team_provisioning"
or "auto-provisioned" in tags
)
def _specialist_id(self, role: str, task: str) -> str:
base = normalize_subagent_id(role)
return normalize_subagent_id(f"{base}-{self._fingerprint(task)}")
@staticmethod
def _fingerprint(task: str) -> str:
return hashlib.sha1(str(task or "").encode("utf-8")).hexdigest()[:8]
@staticmethod
def _display_name(role: str) -> str:
return " ".join(part.capitalize() for part in re.split(r"[\s_-]+", role.strip()) if part)
def _system_prompt(self, role: str, task: str, skills: list[str]) -> str:
# skills 是本次 team run 要求携带的技能上下文;这里仅写入提示词,
# 真正的工具可用性和权限仍由外层 AgentLoop / tool registry 控制。
skills_text = ", ".join(skills) if skills else "none"
role_text = re.sub(r"\s+", " ", str(role or "").strip()) or "general specialist"
# 这里保持一套完全通用的提示模板:
# - 不对具体角色做领域特化
# - 不规定固定输出格式
# - 只强调“按该角色名称隐含的职责边界来贡献结果”
return (
f"你是 nanobot agent team 中的 {role_text}\n\n"
"请围绕这个角色名称所隐含的职责边界处理原始团队任务。根据任务本身选择"
"合适的方法、工具、下游委派方式和输出格式,不要强行套用固定报告模板。"
"你的结果应该便于团队合并成最终答案;如果关键假设、阻塞点或风险会影响"
"结论,请明确指出。\n\n"
f"原始团队任务:\n{task}\n\n"
f"本次要求的技能:\n{skills_text}"
)

View File

@ -0,0 +1,261 @@
# Agent Team 真实运行调用链
更新时间2026-04-08
这份文档用于代码 review。它不再写伪代码流程图而是按当前实现列出从 `spawn_agent_team` 被调用,到 swarms 多 agent 执行,再到结果公告和持久化的真实函数链路。
核心原则:
```text
nanobot 负责入口、registry、权限、skills、事件、memory、BridgeResult。
swarms 负责团队架构运行、agent 间讨论/编排、调用 adapter。
```
## 主调用链
```text
SpawnAgentTeamTool.execute()
作用LLM/tool 层入口,接收 task / label / skills。
-》 DelegationManager.dispatch_agent_team()
作用:把工具调用转换成 agent_team 委派请求,固定 mode="agent_team"、strategy="group"。
-》 DelegationManager._dispatch()
作用:生成 run_id、display_label、origin创建后台 asyncio task立即返回“Agent team started”。
-》 DelegationManager._run_dispatch()
作用:后台真正执行 agent_team 分支;发出团队开始事件,并把任务交给 orchestrator。
-》 AgentTeamOrchestrator.run_task()
作用agent team 薄编排入口;只做 plan -> swarms -> memory不自建 team runtime。
-》 SwarmsRunPlanner.plan()
作用:生成 SwarmsRunSpec决定 swarm_type、agent_ids、skills、rules、max_loops。
-》 SwarmsBridge.run_spec()
作用:发出“启动 swarms runtime”事件执行 swarms并把 swarms 输出转成 BridgeResult。
-》 SwarmsBridge._run_swarms()
作用:把 SwarmsRunSpec.agent_ids 转成 AgentDescriptor再包成 NanobotAgentAdapter。
-》 load_swarms_runtime()
作用:懒加载 vendored third_party/swarms取 AutoSwarmBuilder / SwarmRouter / GroupChat。
-》 swarms.SwarmRouter(...)
作用:创建 swarms 统一路由器,传入 nanobot adapters、swarm_type、rules、max_loops。
-》 SwarmRouter.run(task=...)
作用:交给 swarms 运行对应架构,例如 GroupChat / SequentialWorkflow / ConcurrentWorkflow。
-》 NanobotAgentAdapter.run()
作用swarms 调用每个 agent adapteradapter 把 swarms conversation context 转回 nanobot 成员任务。
-》 DelegationManager._run_team_member_for_swarms()
作用:为该成员创建 child run做权限检查发 agent started/finished 事件。
-》 DelegationManager._execute_descriptor()
作用:真正执行成员 agentlocal_prompt/local_fallback 走 local_executorA2A agent 走 A2AClient。
-》 local_executor.run_local_task() 或 A2AClient.run_task()
作用:成员 agent 产出 AgentRunResult。
-》 NanobotAgentAdapter.run()
作用:收集 AgentRunResult 到 adapter.results并把 summary 返回给 swarms。
-》 SwarmRouter.run(task=...)
作用swarms 收集所有 adapter 响应,返回 raw_output/transcript。
-》 SwarmsBridge._normalize_swarms_output()
作用:优先用 adapter.results 生成可读 SwarmsRunResult.summary并保留 raw_output。
-》 SwarmsBridge.run_spec()
作用:构造 BridgeAttempt、candidate ProcedureRecord、BridgeResult。
-》 AgentTeamOrchestrator.run_task()
作用:成功时 ProcedureMemory.record_candidate(),随后 RunMemory.record_run(),再返回 BridgeResult。
-》 DelegationManager._run_dispatch()
作用:发团队 finished 事件,并调用 _announce_orchestrator_result()。
-》 DelegationManager._announce_orchestrator_result()
作用:把 BridgeResult 组装成给主 agent 的总结消息。
-》 DelegationManager._publish_announcement() 或 _notify_direct_announcement()
作用:通过 bus 回流主 agent或直连回调到本地会话。
-》 DelegationManager._emit_direct_user_message()
作用:如果有 process event sink给 UI 发即时可见完成消息。
```
## Plan 分支
`SwarmsRunPlanner.plan()` 内部有两个分支。
简单/常规任务:
```text
SwarmsRunPlanner.plan()
作用:读取 ProcedureMemory.match_procedure(task),判断不需要 AutoSwarmBuilder。
-》 SwarmsRunPlanner._simple_required_roles()
作用:从 skills 生成角色,例如 implementation specialist / test specialist没有 skills 则用 general specialist / synthesis analyst。
-》 TargetResolver.resolve_team_targets()
作用:根据 task、skills、required_specialists 选择已有 registry agents缺人时调用 provisioning。
-》 AgentRegistry.suggest_agents() / AgentRegistry.get_agent()
作用:从 workspace/plugin/skill/local registry 中查找可执行 agent。
-》 ProvisioningManager.ensure_local_specialist()
作用:缺少合适 agent 时创建 managed local A2A specialist并写入 workspace agent registry。
-》 SwarmsRunSpec(...)
作用:返回默认 GroupChat 运行规格,带 agent_ids、skills、rules、target_plan metadata。
```
复杂/开放任务:
```text
SwarmsRunPlanner.plan()
作用:如果任务较长、命中复杂关键词,或有 ProcedureMemory hint则进入自动建队。
-》 SwarmsRunPlanner._run_auto_swarm_builder()
作用:调用 swarms.AutoSwarmBuilder 生成 router config 建议。
-》 SwarmsRunPlanner._auto_builder_prompt()
作用:把 task、skills、memory_hint 和硬约束写入 AutoSwarmBuilder prompt。
-》 SwarmsPolicy.validate_auto_config()
作用:只允许安全的 swarm_type限制 max_agents/max_loops剥掉 tools、MCP、API key 等越权字段。
-》 SwarmsRunPlanner._roles_from_auto_config()
作用:从 AutoSwarmBuilder 输出提取需要的角色描述。
-》 TargetResolver.resolve_team_targets()
作用:把角色描述映射成 nanobot registry 中真实可执行的 agent_ids。
-》 SwarmsRunPlanner._rearrange_flow()
作用:如果 swarm_type 是 AgentRearrange则用 safe_swarms_name(agent_id) 生成 flow。
-》 SwarmsRunSpec(...)
作用:返回经过 policy 清洗后的 swarms 运行规格。
```
## Swarms 执行链
```text
SwarmsBridge.run_spec()
作用:接收 SwarmsRunSpec发 process_run_progress(stage_label="启动 swarms runtime")。
-》 SwarmsBridge._run_swarms()
作用:解析 spec.agent_ids构造 adapters并实例化 SwarmRouter。
-》 NanobotAgentAdapter.__post_init__()
作用:设置 swarms 可识别的 agent_name/name/__name__/system_prompt。
-》 SwarmsBridge._rules_with_skills()
作用:生成 swarms rules加入“不要新增工具/凭证/外部 endpoint”和 skills 约束。
-》 SwarmsBridge._task_with_skills()
作用:把 spec.task 和 spec.skills 合并成传给 SwarmRouter.run(task=...) 的任务文本。
-》 SwarmRouter.run(task=...)
作用swarms 按 spec.swarm_type 创建并运行实际 swarm。
-》 GroupChat / SequentialWorkflow / ConcurrentWorkflow / AgentRearrange / MixtureOfAgents / HierarchicalSwarm
作用:由 swarms 负责具体多 agent 架构的讨论、顺序、并行、动态流程或层级协作。
-》 NanobotAgentAdapter.run()
作用:当 swarms 需要某个 agent 响应时,调用 nanobot adapter。
-》 SwarmsBridge._normalize_swarms_output()
作用:把 swarms raw_output 和 adapter.results 合并成 SwarmsRunResult。
-》 SwarmsBridge._candidate_procedure()
作用:成功时构造可选 ProcedureRecord供 ProcedureMemory 学习复用。
-》 BridgeResult(...)
作用:统一返回 success、summary、member_results、candidate_procedure、attempts、raw。
```
## 成员执行链
```text
NanobotAgentAdapter.run(task)
作用:接收 swarms 传入的 conversation/task。
-》 NanobotAgentAdapter._task_with_skills()
作用:把 skills 注入成员任务文本,形成 delegated_task。
-》 asyncio.run_coroutine_threadsafe(member_runner(...))
作用:从 swarms 的同步调用线程切回 nanobot 当前事件循环。
-》 DelegationManager._run_team_member_for_swarms(descriptor, task, parent_run_id, skills)
作用:创建 child_run_id保持父子 process tree。
-》 DelegationManager._ensure_descriptor_allowed()
作用:检查 local/plugin/A2A agent 是否允许被委派。
-》 DelegationManager._emit_agent_started()
作用:发出成员开始事件。
-》 DelegationManager._execute_descriptor()
作用:根据 AgentDescriptor.kind / protocol 选择执行方式。
-》 local_executor.run_local_task()
作用:执行 local_prompt / local_fallback agent并传入 skill_context、skill_names、progress_callback。
-》 A2AClient.run_task()
作用:执行远端或本地 gateway 暴露的 A2A agent。
-》 DelegationManager._emit_agent_finished()
作用:发出成员完成事件。
-》 NanobotAgentAdapter.run()
作用:把 AgentRunResult 存入 adapter.results成功时返回 result.summary失败时返回 error 文本给 swarms。
```
## skills 注入链
```text
SpawnAgentTeamTool.execute(skills)
作用:接收工具参数里的 skills。
-》 DelegationManager.dispatch_agent_team(skills=skills)
作用:把 skills 放进后台 dispatch 参数。
-》 DelegationManager._dispatch(skills=skills)
作用:把 skills 保存到后台 task 调用参数。
-》 DelegationManager._run_dispatch(skills=skills)
作用:把 skills 传给 AgentTeamOrchestrator.run_task()。
-》 AgentTeamOrchestrator.run_task(skills=skills)
作用:把 skills 传给 planner 和 swarms bridge。
-》 SwarmsRunPlanner.plan(skills=skills)
作用skills 参与角色选择和 AutoSwarmBuilder prompt。
-》 SwarmsRunSpec.skills
作用skills 固化到运行规格,供 events、rules、task、adapter 使用。
-》 SwarmsBridge._rules_with_skills()
作用:把 skills 写入 SwarmRouter rules。
-》 SwarmsBridge._task_with_skills()
作用:把 skills 写入 SwarmRouter.run(task=...) 的任务文本。
-》 NanobotAgentAdapter._task_with_skills()
作用:把 skills 写入每个成员看到的 delegated task。
-》 DelegationManager._execute_descriptor(skill_names=skills)
作用:本地 agent 获得 skill_context / skill_namesA2A agent 获得 augment 后的任务文本。
```
## 结果返回链
```text
SwarmsBridge._normalize_swarms_output()
作用:生成 SwarmsRunResult(summary, raw_output, member_results)。
-》 SwarmsBridge.run_spec()
作用:生成 BridgeAttempt 和 BridgeResult。
-》 AgentTeamOrchestrator.run_task()
作用:写 ProcedureMemory 和 RunMemory。
-》 DelegationManager._emit_group_finished()
作用:把团队 run 标记为 done/errormetadata 带 attempts 和成员状态。
-》 DelegationManager._announce_orchestrator_result()
作用:把 BridgeResult 整理成主 agent 可读的系统消息。
-》 DelegationManager._publish_announcement()
作用announce_via_bus=True 时,把消息 publish 到 inbound bus让主 agent 继续总结。
-》 DelegationManager._notify_direct_announcement()
作用announce_via_bus=False 时,直接调用本地回调回流会话。
-》 DelegationManager._emit_direct_user_message()
作用:有 process event sink 时,给前端/UI 发一条即时完成消息。
```
## 当前放行的 swarms 架构
`SwarmsPolicy.allowed_swarm_types` 当前只放行能消费 nanobot adapters 的架构:
```text
GroupChat
SequentialWorkflow
ConcurrentWorkflow
AgentRearrange
MixtureOfAgents
HierarchicalSwarm
```
`GraphWorkflow` / `HeavySwarm` 暂不直接放行,因为当前 vendored `SwarmRouter` 的相关 factory 还不能稳定消费 nanobot 提供的 `NanobotAgentAdapter`、registry、skills 和权限边界。
## 文件职责速查
```text
agent/tools/spawn.py
作用:定义 spawn_agent_team 工具入口。
agent/delegation.py
作用后台调度、process events、成员执行、结果公告。
agent_team/orchestrator.py
作用agent team 主 glue负责 plan -> swarms -> memory。
agent_team/swarms_planner.py
作用:生成 SwarmsRunSpec需要时调用 AutoSwarmBuilder。
agent_team/swarms_policy.py
作用:清洗 AutoSwarmBuilder 输出,限制 swarm_type、agents、loops 和越权字段。
agent_team/target_resolver.py
作用:把角色需求解析成真实 agent_ids。
agent_team/provisioning.py
作用:缺少合适成员时创建 managed local A2A specialist。
agent_team/swarms_adapter.py
作用:懒加载 vendored swarms并把 nanobot agent 包成 swarms 可调用 adapter。
agent_team/swarms_bridge.py
作用:构造 SwarmRouter、运行 swarms、归一化 BridgeResult。
agent_team/memory.py
作用:记录 RunMemory / ProcedureMemory。
agent_team/types.py
作用:定义 SwarmsRunSpec、SwarmsRunResult、BridgeAttempt、BridgeResult 等共享类型。
```

View File

@ -0,0 +1,114 @@
"""Thin adapters between nanobot agents and the vendored swarms runtime."""
from __future__ import annotations
import asyncio
import sys
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from nanobot.agent.agent_registry import AgentDescriptor
from nanobot.agent.run_result import AgentRunResult
MemberRunner = Callable[[AgentDescriptor, str, str, list[str]], Awaitable[AgentRunResult]]
def _candidate_swarms_roots() -> list[Path]:
"""Return likely vendored swarms paths across source and packaged layouts."""
module_path = Path(__file__).resolve()
candidates = [
module_path.parents[2] / "third_party" / "swarms",
Path("/opt/app/backend/third_party/swarms"),
Path("/app/third_party/swarms"),
Path.cwd() / "third_party" / "swarms",
Path.cwd() / "backend" / "third_party" / "swarms",
]
unique: list[Path] = []
seen: set[str] = set()
for candidate in candidates:
key = str(candidate)
if key in seen:
continue
seen.add(key)
unique.append(candidate)
return unique
def ensure_swarms_importable() -> None:
"""Put the vendored swarms checkout on `sys.path` if needed."""
for swarms_root in _candidate_swarms_roots():
if swarms_root.exists() and str(swarms_root) not in sys.path:
sys.path.insert(0, str(swarms_root))
return
def load_swarms_runtime() -> dict[str, Any]:
"""Lazy-load swarms classes without making package import fragile."""
ensure_swarms_importable()
from swarms import AutoSwarmBuilder # type: ignore
from swarms.structs.groupchat import GroupChat # type: ignore
from swarms.structs.swarm_router import SwarmRouter # type: ignore
return {
"AutoSwarmBuilder": AutoSwarmBuilder,
"GroupChat": GroupChat,
"SwarmRouter": SwarmRouter,
}
def __getattr__(name: str) -> Any:
if name in {"AutoSwarmBuilder", "GroupChat", "SwarmRouter"}:
return load_swarms_runtime()[name]
raise AttributeError(name)
def safe_swarms_name(agent_id: str) -> str:
"""Return a GroupChat-friendly ASCII-ish name for @mentions."""
normalized = "".join(ch if ch.isalnum() else "_" for ch in str(agent_id or "agent"))
normalized = normalized.strip("_") or "agent"
return f"agent_{normalized}"
@dataclass(eq=False)
class NanobotAgentAdapter:
"""Callable wrapper that lets swarms invoke a nanobot agent descriptor."""
descriptor: AgentDescriptor
run_id: str
loop: asyncio.AbstractEventLoop
member_runner: MemberRunner
skills: list[str]
results: list[AgentRunResult] = field(default_factory=list, init=False)
def __post_init__(self) -> None:
self.agent_name = safe_swarms_name(self.descriptor.id)
self.name = self.agent_name
self.system_prompt = self.descriptor.system_prompt or self.descriptor.description
self.__name__ = self.agent_name
def __call__(self, conversation_context: str) -> str:
return self.run(conversation_context)
def run(self, task: str, *args: Any, **kwargs: Any) -> str:
delegated_task = self._task_with_skills(task)
future = asyncio.run_coroutine_threadsafe(
self.member_runner(self.descriptor, delegated_task, self.run_id, list(self.skills)),
self.loop,
)
result = future.result(timeout=300)
self.results.append(result)
if result.status != "ok":
return f"Error from {self.agent_name}: {result.summary}"
return result.summary
def _task_with_skills(self, conversation_context: str) -> str:
if not self.skills:
return conversation_context
return (
"Required skills for this delegated team member:\n"
f"{', '.join(self.skills)}\n\n"
"Swarms conversation context:\n"
f"{conversation_context}"
).strip()

View File

@ -0,0 +1,302 @@
"""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 {}),
},
)

View File

@ -0,0 +1,184 @@
"""Planner that prepares a minimal swarms run spec for agent-team tasks."""
from __future__ import annotations
import asyncio
import json
from typing import Any
from loguru import logger
from nanobot.agent.agent_registry import AgentRegistry
from nanobot.agent_team.memory import ProcedureMemory
from nanobot.agent_team.swarms_adapter import load_swarms_runtime, safe_swarms_name
from nanobot.agent_team.swarms_policy import SwarmsPolicy
from nanobot.agent_team.target_resolver import TargetResolver
from nanobot.agent_team.types import SwarmsRunSpec
class SwarmsRunPlanner:
"""Generate `SwarmsRunSpec` without rebuilding swarms' own planner/runtime."""
def __init__(
self,
*,
model: str | None,
registry: AgentRegistry,
target_resolver: TargetResolver,
procedure_memory: ProcedureMemory,
policy: SwarmsPolicy,
) -> None:
self.model = model
self.registry = registry
self.target_resolver = target_resolver
self.procedure_memory = procedure_memory
self.policy = policy
async def plan(self, *, task: str, label: str, skills: list[str]) -> SwarmsRunSpec:
memory_hint = self.procedure_memory.match_procedure(task)
if self._should_auto_build(task, skills, memory_hint):
raw_config = await self._run_auto_swarm_builder(task, skills, memory_hint)
return await self._spec_from_auto_config(task, label, skills, raw_config)
target_plan = await self.target_resolver.resolve_team_targets(
task=task,
skills=skills,
required_specialists=self._simple_required_roles(task, skills),
)
return SwarmsRunSpec(
task=task,
label=label,
skills=list(skills),
swarm_type="GroupChat",
agent_ids=list(target_plan.final_targets),
auto_generated=False,
max_loops=2,
rules=self._default_rules(),
metadata={
"memory_hint": memory_hint.id if memory_hint else None,
"target_plan": target_plan.to_dict(),
},
)
def _should_auto_build(self, task: str, skills: list[str], memory_hint: Any) -> bool:
source = task or ""
text = source.lower()
markers = ("架构", "调研", "复杂", "多阶段", "strategy", "architecture", "research")
return len(source) > 80 or memory_hint is not None or any(
marker in source or marker in text for marker in markers
)
async def _run_auto_swarm_builder(self, task: str, skills: list[str], memory_hint: Any) -> dict[str, Any]:
try:
runtime = load_swarms_runtime()
builder = runtime["AutoSwarmBuilder"](
name="nanobot-auto-swarm-builder",
description="Generate a safe swarms router config for nanobot",
max_loops=1,
model_name=self._auto_builder_model_name(),
generate_router_config=True,
execution_type="return-swarm-router-config",
interactive=False,
verbose=False,
)
raw = await asyncio.to_thread(
builder.run,
self._auto_builder_prompt(task, skills, memory_hint),
)
if isinstance(raw, dict):
return raw
if isinstance(raw, str):
return json.loads(raw)
model_dump = getattr(raw, "model_dump", None)
if callable(model_dump):
payload = model_dump()
return payload if isinstance(payload, dict) else {}
except Exception as exc:
logger.warning("AutoSwarmBuilder failed; falling back to deterministic run spec: {}", exc)
return {}
def _auto_builder_model_name(self) -> str:
model_name = str(self.model or "").strip()
if not model_name:
return "gpt-4.1"
if "/" in model_name:
return model_name
return f"openai/{model_name}"
def _auto_builder_prompt(self, task: str, skills: list[str], memory_hint: Any) -> str:
return (
"Build a multi-agent swarm router config for nanobot.\n\n"
f"User task:\n{task}\n\n"
f"Required nanobot skills:\n{skills}\n\n"
f"Procedure memory hint:\n{memory_hint}\n\n"
"Return a valid JSON object that matches the swarm router config schema.\n\n"
"Hard constraints:\n"
"- Every generated role must follow the listed skills.\n"
"- Do not replace, ignore, or reinterpret the listed skills.\n"
"- Do not add external tools, credentials, MCP URLs, or hidden side effects.\n"
"- Prefer existing nanobot registry agents; only describe missing roles."
)
async def _spec_from_auto_config(
self,
task: str,
label: str,
skills: list[str],
raw_config: dict[str, Any],
) -> SwarmsRunSpec:
safe_config = self.policy.validate_auto_config(raw_config)
target_plan = await self.target_resolver.resolve_team_targets(
task=task,
skills=skills,
required_specialists=self._roles_from_auto_config(safe_config),
)
return SwarmsRunSpec(
task=task,
label=label,
skills=list(skills),
swarm_type=str(safe_config.get("swarm_type") or "GroupChat"),
agent_ids=list(target_plan.final_targets),
auto_generated=bool(raw_config),
max_loops=min(int(safe_config.get("max_loops") or 2), self.policy.max_loops),
rearrange_flow=self._rearrange_flow(safe_config, target_plan.final_targets),
rules=str(safe_config.get("rules") or self._default_rules()),
raw_auto_config=safe_config,
metadata={
"target_plan": target_plan.to_dict(),
"auto_builder_returned_config": bool(raw_config),
},
)
def _rearrange_flow(self, config: dict[str, Any], agent_ids: list[str]) -> str | None:
if str(config.get("swarm_type") or "") == "AgentRearrange" and agent_ids:
return " -> ".join(safe_swarms_name(agent_id) for agent_id in agent_ids)
flow = config.get("rearrange_flow") or config.get("flow")
if flow:
return str(flow)
return None
def _roles_from_auto_config(self, config: dict[str, Any]) -> list[str]:
roles: list[str] = []
for item in config.get("agents", []) or []:
if not isinstance(item, dict):
continue
role = str(
item.get("description")
or item.get("system_prompt")
or item.get("agent_name")
or ""
).strip()
if role:
roles.append(role)
return roles or ["general specialist", "synthesis analyst"]
def _simple_required_roles(self, task: str, skills: list[str]) -> list[str]:
if skills:
return [f"{skill} specialist" for skill in skills]
return ["general specialist", "synthesis analyst"]
def _default_rules(self) -> str:
return (
"You are running inside a nanobot agent team. Follow the provided skills, "
"stay within your assigned role, and produce a concise final synthesis."
)

View File

@ -0,0 +1,70 @@
"""Policy guardrails for swarms-generated agent team plans."""
from __future__ import annotations
from typing import Any
class SwarmsPolicy:
"""Clamp AutoSwarmBuilder output before nanobot executes it."""
allowed_swarm_types = {
# Keep this list to swarms that consume the provided nanobot agent adapters.
"GroupChat",
"SequentialWorkflow",
"ConcurrentWorkflow",
"AgentRearrange",
"MixtureOfAgents",
"HierarchicalSwarm",
}
def __init__(self, *, max_agents: int = 4, max_loops: int = 3) -> None:
self.max_agents = max(1, max_agents)
self.max_loops = max(1, max_loops)
def validate_auto_config(self, raw_config: dict[str, Any]) -> dict[str, Any]:
config = self._plain_dict(raw_config)
swarm_type = str(
config.get("swarm_type")
or config.get("type")
or config.get("architecture")
or "GroupChat"
)
if swarm_type not in self.allowed_swarm_types:
swarm_type = "GroupChat"
config["swarm_type"] = swarm_type
agents = list(config.get("agents") or [])[: self.max_agents]
config["agents"] = [self._sanitize_agent_spec(item) for item in agents]
config["max_loops"] = min(max(1, int(config.get("max_loops") or 2)), self.max_loops)
# AutoSwarmBuilder may suggest structure, not grant capabilities.
config.pop("tools", None)
config.pop("mcp_url", None)
config.pop("mcp_urls", None)
config.pop("llm_api_key", None)
config.pop("api_key", None)
return config
def _plain_dict(self, raw_config: Any) -> dict[str, Any]:
if isinstance(raw_config, dict):
return dict(raw_config)
model_dump = getattr(raw_config, "model_dump", None)
if callable(model_dump):
payload = model_dump()
return dict(payload) if isinstance(payload, dict) else {}
dict_method = getattr(raw_config, "dict", None)
if callable(dict_method):
payload = dict_method()
return dict(payload) if isinstance(payload, dict) else {}
return {}
def _sanitize_agent_spec(self, item: Any) -> dict[str, Any]:
spec = self._plain_dict(item)
return {
"agent_name": str(spec.get("agent_name") or spec.get("name") or "specialist"),
"description": str(spec.get("description") or spec.get("agent_description") or ""),
"system_prompt": str(spec.get("system_prompt") or "")[:4000],
"role": str(spec.get("role") or "worker"),
}

View File

@ -0,0 +1,267 @@
"""Resolve and provision team targets before execution.
该模块负责在真正启动 agent-team / swarms 执行前,把“任务需要哪些角色”
转换成一组可执行的 agent id。它优先复用 registry 里已有的 agent当没有合适
agent 覆盖某个角色时,再通过 ProvisioningManager 在本地创建 A2A specialist。
"""
from __future__ import annotations
from pathlib import Path
from loguru import logger
from nanobot.agent.agent_registry import AgentDescriptor, AgentRegistry
from nanobot.agent_team.provisioning import ProvisioningManager
from nanobot.agent_team.types import ResolvedTeamPlan
from nanobot.providers.base import LLMProvider
class TargetResolver:
"""把任务级的 specialist 需求解析成最终可执行的 agent id 列表。
解析策略分两层:
1. 先读取当前 registry 里所有可见 agent并过滤掉 router/planner 等
不适合作为群聊工作成员的 agent。
2. 如果调用方明确给出 required_specialists则把 role 和候选 agent 交给
LLM 直接选择最合适的已有 agentLLM 选不出来时才 provision 本地
specialist。没有明确角色时则直接使用过滤后的已有 agent若为空再
兜底创建 general specialist。
"""
def __init__(
self,
*,
workspace: Path,
registry: AgentRegistry,
provider: LLMProvider,
model: str | None = None,
max_parallel_agents: int = 16,
gateway_port: int = 18790,
provisioning: ProvisioningManager | None = None,
) -> None:
# max_parallel_agents 同时限制“最多尝试的角色数”和“最终返回的 agent 数”,
# 避免一次 team run 生成过多并行成员。
self.workspace = workspace
self.registry = registry
self.provider = provider
self.model = model or provider.get_default_model()
self.max_parallel_agents = max(1, max_parallel_agents)
self.provisioning = provisioning or ProvisioningManager(workspace, gateway_port=gateway_port)
async def resolve_team_targets(
self,
*,
task: str,
skills: list[str] | None = None,
required_specialists: list[str] | None = None,
) -> ResolvedTeamPlan:
"""解析一次 team run 的目标 agent。
Args:
task: 用户原始任务,用于 LLM 选 agent 和 specialist provision prompt。
skills: 本次任务要求携带的技能列表,会传给新 provision 的 specialist。
required_specialists: 上游 planner 推导出的角色需求。例如来自
AutoSwarmBuilder config 的 agent description或 skills 的简单映射。
Returns:
ResolvedTeamPlan: 包含已复用 agent、已 provision agent、最终执行目标、
选择理由和审计 metadata。
"""
# 清理空字符串/空白角色,避免后续创建出没有意义的 specialist。
required = [item for item in (required_specialists or []) if str(item).strip()]
# 直接读取 registry 当前所有可见 agent再过滤掉 router、planner、
# local-subagent 这类不适合作为 swarms/group worker 的 agent。
suggestions = [
agent
for agent in self.registry.list_agents(include_local_fallback=False)
if self._is_group_worker_candidate(agent)
]
# selected: 从 registry 复用的已有 agent id。
# covered_roles: 哪些 required role 已经被已有 agent 覆盖,用于 metadata。
# provisioned: 为缺失角色新建/确保存在的本地 specialist id。
# created_provisioned: 本次 run 真正新建出来的 specialist id后续自动清理只看它
# 避免把之前已经存在、只是被刷新/复用的 specialist 误删。
# actions: provision 审计记录,方便上层解释“为什么创建了某个 agent”。
selected: list[str] = []
covered_roles: list[str] = []
provisioned: list[str] = []
created_provisioned: list[str] = []
actions: list[dict[str, str]] = []
if required:
# 调用方给出了明确角色时,不再做本地词法规则匹配,而是直接把
# role + task + 候选 agent 交给 LLM 判断最适合复用哪个已有 agent。
# 这里切片是为了遵守 max_parallel_agents 上限。
for role in required[: self.max_parallel_agents]:
existing = await self._select_existing_for_role_with_llm(
task=task,
role=role,
suggestions=suggestions,
selected=selected,
)
if existing is not None:
selected.append(existing.id)
covered_roles.append(role)
continue
provision_result = await self.provisioning.ensure_local_specialist_with_result(
role=role,
task=task,
skills=skills or [],
)
agent_id = provision_result.agent_id
provisioned.append(agent_id)
if provision_result.created:
created_provisioned.append(agent_id)
actions.append({
"action": "ensure_local_specialist",
"role": role,
"agent_id": agent_id,
"created": str(provision_result.created).lower(),
})
else:
# 没有明确角色需求时,直接使用当前可见的已有 agent最多取并行上限。
selected = [agent.id for agent in suggestions[: self.max_parallel_agents]]
if not selected:
# 当前 registry 没有可用 worker 时,创建一个通用 specialist 作为最低可执行兜底。
provision_result = await self.provisioning.ensure_local_specialist_with_result(
role="general specialist",
task=task,
skills=skills or [],
)
agent_id = provision_result.agent_id
provisioned.append(agent_id)
if provision_result.created:
created_provisioned.append(agent_id)
actions.append({
"action": "ensure_local_specialist",
"role": "general specialist",
"agent_id": agent_id,
"created": str(provision_result.created).lower(),
})
# 合并已有 agent 和新 provision 的 agent
# - dict.fromkeys 保留顺序并去重,避免同一个 agent 被重复加入;
# - 最后再次截断,防止 selected + provisioned 总数超过并行上限。
final_targets = list(dict.fromkeys([*selected, *provisioned]))[: self.max_parallel_agents]
# selection_reason 是给上层/日志展示的粗粒度解释metadata 里会保留更细的明细。
reason = (
"已选择现有 registry agent。"
if selected and not provisioned
else "已选择现有 registry agent并为缺失角色补充了 specialist。"
if selected and provisioned
else "没有匹配到合适的现有 agent已补充本地 A2A specialist。"
if provisioned
else "没有匹配到合适的现有 agent且未补充任何 specialist。"
)
logger.info(
"Resolved agent-team targets selected={} provisioned={} final={}",
selected,
provisioned,
final_targets,
)
# ResolvedTeamPlan 是后续 orchestrator/swarms planner 使用的稳定边界:
# final_targets 用于实际执行selected/provisioned/actions/metadata 用于解释和调试。
return ResolvedTeamPlan(
selected_existing_targets=selected,
provisioned_targets=provisioned,
created_provisioned_targets=created_provisioned,
final_targets=final_targets,
selection_reason=reason,
provision_actions=actions,
metadata={
"required_specialists": required,
"available_agent_count": len(suggestions),
"covered_roles": covered_roles,
"created_provisioned_targets": created_provisioned,
"max_parallel_agents": self.max_parallel_agents,
},
)
@staticmethod
def _is_group_worker_candidate(agent: AgentDescriptor) -> bool:
"""判断一个 registry agent 是否适合作为 team/group worker。
router/planner 类 agent 通常负责调度,不应被当作普通成员加入 GroupChat 或
swarms worker 列表local-subagent 是通用本地代理入口,也避免在这里重复选中。
"""
probe = " ".join([
agent.id,
agent.name,
agent.description,
" ".join(agent.tags),
" ".join(agent.aliases),
]).lower()
if agent.id == "local-subagent":
return False
return not any(marker in probe for marker in ("chat-router", "router", "planner"))
async def _select_existing_for_role_with_llm(
self,
*,
task: str,
role: str,
suggestions: list[AgentDescriptor],
selected: list[str],
) -> AgentDescriptor | None:
"""让 LLM 从已有候选 agent 中为 role 选择最合适的一个。"""
candidates = [agent for agent in suggestions if agent.id not in selected]
if not candidates:
return None
if len(candidates) == 1:
return candidates[0]
lines = []
for agent in candidates:
tags = ", ".join(agent.tags) if agent.tags else "none"
aliases = ", ".join(agent.aliases) if agent.aliases else "none"
lines.append(
f"- id: {agent.id}\n"
f" name: {agent.name}\n"
f" description: {agent.description}\n"
f" tags: {tags}\n"
f" aliases: {aliases}"
)
try:
response = await self.provider.chat(
messages=[
{
"role": "system",
"content": (
"You select one existing agent for a required team role.\n"
"Return exactly one agent id from the candidate list, or NONE.\n"
"Do not explain your reasoning."
),
},
{
"role": "user",
"content": (
f"Task:\n{task}\n\n"
f"Required role:\n{role}\n\n"
"Candidates:\n"
f"{chr(10).join(lines)}\n\n"
"Return exactly one candidate id, or NONE if none of them clearly fits."
),
},
],
model=self.model,
temperature=0,
max_tokens=32,
)
except Exception as exc:
logger.warning("LLM role selection failed for role '{}': {}", role, exc)
return None
raw = str(response.content or "").strip()
choice = raw.splitlines()[0].strip().strip("`'\"") if raw else ""
candidate_map = {agent.id: agent for agent in candidates}
if choice in candidate_map:
return candidate_map[choice]
if choice.upper() not in {"", "NONE"}:
logger.info("LLM role selection returned unknown agent id '{}' for role '{}'", choice, role)
return None

View File

@ -0,0 +1,546 @@
"""Agent Team swarms 适配层的共享类型定义。"""
from __future__ import annotations
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from enum import Enum
from typing import Any
from nanobot.agent.run_result import AgentRunResult
def now_iso() -> str:
"""返回统一格式的 UTC 时间戳字符串。
Demo 输出:
`2026-03-31T12:00:00.000000+00:00`
"""
# 统一使用 UTC避免跨机器或跨时区比较 run/procedure 时间时出现歧义。
return datetime.now(timezone.utc).isoformat()
def new_record_id(prefix: str) -> str:
"""为 memory 记录生成短 ID。
Demo 输出:
`procedure-3fa2c7b1`
"""
# 这里保留可读前缀,方便磁盘文件、日志和测试断言定位数据来源。
return f"{prefix}-{uuid.uuid4().hex[:8]}"
def agent_result_to_dict(result: AgentRunResult) -> dict[str, Any]:
"""把 `AgentRunResult` 转成可 JSON 序列化的字典。
Demo 输出:
`{"agent_id": "writer", "agent_name": "Writer", "status": "ok", "summary": "...", "raw": {}}`
"""
# `raw` 允许为空,这里统一转成字典或 None避免后续序列化分支散落各处。
return {
"agent_id": result.agent_id,
"agent_name": result.agent_name,
"status": result.status,
"summary": result.summary,
"raw": result.raw,
}
def agent_result_from_dict(payload: dict[str, Any]) -> AgentRunResult:
"""从字典重建 `AgentRunResult`。
Demo 输出:
`AgentRunResult(agent_id="writer", agent_name="Writer", status="ok", summary="...", raw=None)`
"""
# 所有字段都做最小兜底,防止历史磁盘记录缺字段时直接炸掉整个读取流程。
return AgentRunResult(
agent_id=str(payload.get("agent_id") or "unknown-agent"),
agent_name=str(payload.get("agent_name") or payload.get("agent_id") or "Unknown Agent"),
status=str(payload.get("status") or "error"),
summary=str(payload.get("summary") or ""),
raw=payload.get("raw") if isinstance(payload.get("raw"), dict) else None,
)
class ExecutionMode(str, Enum):
"""编排器支持的执行模式。"""
SWARMS = "swarms"
def parse_execution_mode(value: Any, default: ExecutionMode = ExecutionMode.SWARMS) -> ExecutionMode:
"""把持久化里的 mode 字符串解析成 ExecutionMode。"""
raw = str(value or default.value)
try:
return ExecutionMode(raw)
except ValueError:
return default
@dataclass(slots=True)
class ResolvedTeamPlan:
"""最终执行前解析出的成员计划。"""
selected_existing_targets: list[str] = field(default_factory=list)
provisioned_targets: list[str] = field(default_factory=list)
created_provisioned_targets: list[str] = field(default_factory=list)
final_targets: list[str] = field(default_factory=list)
selection_reason: str = ""
provision_actions: list[dict[str, Any]] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"selected_existing_targets": list(self.selected_existing_targets),
"provisioned_targets": list(self.provisioned_targets),
"created_provisioned_targets": list(self.created_provisioned_targets),
"final_targets": list(self.final_targets),
"selection_reason": self.selection_reason,
"provision_actions": [dict(item) for item in self.provision_actions],
"metadata": dict(self.metadata),
}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> "ResolvedTeamPlan":
return cls(
selected_existing_targets=[
str(item)
for item in payload.get("selected_existing_targets", [])
if str(item).strip()
],
provisioned_targets=[
str(item)
for item in payload.get("provisioned_targets", [])
if str(item).strip()
],
created_provisioned_targets=[
str(item)
for item in payload.get("created_provisioned_targets", [])
if str(item).strip()
],
final_targets=[
str(item)
for item in payload.get("final_targets", [])
if str(item).strip()
],
selection_reason=str(payload.get("selection_reason") or ""),
provision_actions=[
dict(item)
for item in payload.get("provision_actions", [])
if isinstance(item, dict)
],
metadata=payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {},
)
@dataclass(slots=True)
class SwarmsRunSpec:
"""nanobot 交给 swarms runtime 的最小运行规格。"""
task: str
label: str
skills: list[str]
swarm_type: str
agent_ids: list[str]
auto_generated: bool = False
max_loops: int = 2
rearrange_flow: str | None = None
rules: str | None = None
raw_auto_config: dict[str, Any] = field(default_factory=dict)
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"task": self.task,
"label": self.label,
"skills": list(self.skills),
"swarm_type": self.swarm_type,
"agent_ids": list(self.agent_ids),
"auto_generated": self.auto_generated,
"max_loops": self.max_loops,
"rearrange_flow": self.rearrange_flow,
"rules": self.rules,
"raw_auto_config": dict(self.raw_auto_config),
"metadata": dict(self.metadata),
}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> "SwarmsRunSpec":
return cls(
task=str(payload.get("task") or ""),
label=str(payload.get("label") or ""),
skills=[str(item) for item in payload.get("skills", []) if str(item).strip()],
swarm_type=str(payload.get("swarm_type") or "GroupChat"),
agent_ids=[str(item) for item in payload.get("agent_ids", []) if str(item).strip()],
auto_generated=bool(payload.get("auto_generated", False)),
max_loops=max(1, int(payload.get("max_loops") or 2)),
rearrange_flow=str(payload["rearrange_flow"]) if payload.get("rearrange_flow") else None,
rules=str(payload["rules"]) if payload.get("rules") else None,
raw_auto_config=payload.get("raw_auto_config") if isinstance(payload.get("raw_auto_config"), dict) else {},
metadata=payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {},
)
@dataclass(slots=True)
class SwarmsRunResult:
"""swarms runtime 的原始输出归一化前结果。"""
success: bool
summary: str
raw_output: Any
error: str | None = None
member_results: list[AgentRunResult] = field(default_factory=list)
transcript: list[dict[str, Any]] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return {
"success": self.success,
"summary": self.summary,
"raw_output": self.raw_output,
"error": self.error,
"member_results": [agent_result_to_dict(item) for item in self.member_results],
"transcript": [dict(item) for item in self.transcript],
"metadata": dict(self.metadata),
}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> "SwarmsRunResult":
return cls(
success=bool(payload.get("success", False)),
summary=str(payload.get("summary") or ""),
raw_output=payload.get("raw_output"),
error=str(payload["error"]) if payload.get("error") else None,
member_results=[
agent_result_from_dict(item)
for item in payload.get("member_results", [])
if isinstance(item, dict)
],
transcript=[
dict(item)
for item in payload.get("transcript", [])
if isinstance(item, dict)
],
metadata=payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {},
)
@dataclass(slots=True)
class ProcedureRecord:
"""一条可复用的 procedure 记录。
Demo 输出:
`ProcedureRecord(id='procedure-a1b2c3d4', task_template='生成周报', agent_ids=['writer-agent'], strategy='single', confidence=0.65, ...)`
"""
# 稳定主键会被 `RunMemory` 和公告信息引用。
id: str = field(default_factory=lambda: new_record_id("procedure"))
# 原始任务模板用于向后续执行注入“之前学到的做法”。
task_template: str = ""
# 一句话总结这个 procedure 适用的场景和执行方式。
summary: str = ""
# swarms bridge 会按这里列出的 agent 顺序/组合执行。
agent_ids: list[str] = field(default_factory=list)
# 第一版只实现 `single | parallel` 两种策略。
strategy: str = "parallel"
# 用简单关键词做粗粒度匹配,避免引入重型向量索引。
task_keywords: list[str] = field(default_factory=list)
# 置信度用于后续复用和人工排查。
confidence: float = 0.5
# 成功/失败计数用来估算 failure rate。
success_count: int = 0
failure_count: int = 0
# 便于追踪该 procedure 从哪次探索 run 学来。
source_run_id: str | None = None
# 标准时间字段全部保留,方便 UI 或后续排序扩展。
created_at: str = field(default_factory=now_iso)
updated_at: str = field(default_factory=now_iso)
last_used_at: str | None = None
# 额外扩展字段集中收口到 metadata避免频繁改 schema。
metadata: dict[str, Any] = field(default_factory=dict)
def failure_rate(self) -> float:
"""计算该 procedure 的累计失败率。
Demo 输出:
`0.25`
"""
# 没有历史执行时直接返回 0避免“新 procedure 天生失败率 100%”的误判。
total = self.success_count + self.failure_count
if total <= 0:
return 0.0
return self.failure_count / total
def to_dict(self) -> dict[str, Any]:
"""把 procedure 记录转成字典。
Demo 输出:
`{"id": "procedure-a1b2c3d4", "strategy": "parallel", "agent_ids": ["agent-a", "agent-b"], ...}`
"""
return {
"id": self.id,
"task_template": self.task_template,
"summary": self.summary,
"agent_ids": list(self.agent_ids),
"strategy": self.strategy,
"task_keywords": list(self.task_keywords),
"confidence": self.confidence,
"success_count": self.success_count,
"failure_count": self.failure_count,
"source_run_id": self.source_run_id,
"created_at": self.created_at,
"updated_at": self.updated_at,
"last_used_at": self.last_used_at,
"metadata": dict(self.metadata),
}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> "ProcedureRecord":
"""从字典重建 procedure 记录。
Demo 输出:
`ProcedureRecord(id='procedure-a1b2c3d4', task_template='生成周报', ...)`
"""
return cls(
id=str(payload.get("id") or new_record_id("procedure")),
task_template=str(payload.get("task_template") or ""),
summary=str(payload.get("summary") or ""),
agent_ids=[str(item) for item in payload.get("agent_ids", []) if str(item).strip()],
strategy=str(payload.get("strategy") or "parallel"),
task_keywords=[
str(item)
for item in payload.get("task_keywords", [])
if str(item).strip()
],
confidence=float(payload.get("confidence") or 0.5),
success_count=int(payload.get("success_count") or 0),
failure_count=int(payload.get("failure_count") or 0),
source_run_id=str(payload["source_run_id"]) if payload.get("source_run_id") else None,
created_at=str(payload.get("created_at") or now_iso()),
updated_at=str(payload.get("updated_at") or now_iso()),
last_used_at=str(payload["last_used_at"]) if payload.get("last_used_at") else None,
metadata=payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {},
)
@dataclass(slots=True)
class RunRecord:
"""一次 agent team 运行的持久化记录。
Demo 输出:
`RunRecord(id='run-1a2b3c4d', task='生成周报', mode=<ExecutionMode.SWARMS: 'swarms'>, success=True, ...)`
"""
# run 记录也使用短 ID便于文件和日志双向检索。
id: str = field(default_factory=lambda: new_record_id("run"))
# 原始任务文本是最重要的回溯信息,必须完整保留。
task: str = ""
# 执行模式会用于后续做简单统计和问题排查。
mode: ExecutionMode = ExecutionMode.SWARMS
# 归一化成功标记。
success: bool = False
# 最终摘要可直接展示在运维面板或调试脚本里。
summary: str = ""
# 失败时保留错误信息;成功时为 None。
error: str | None = None
# 命中的 procedure 主键,没有命中则为空。
procedure_id: str | None = None
# 记录创建时间。
created_at: str = field(default_factory=now_iso)
# metadata 会保存 attempts、raw 等调试信息。
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
"""把 run 记录转成字典。
Demo 输出:
`{"id": "run-1a2b3c4d", "mode": "swarms", "success": true, ...}`
"""
return {
"id": self.id,
"task": self.task,
"mode": self.mode.value,
"success": self.success,
"summary": self.summary,
"error": self.error,
"procedure_id": self.procedure_id,
"created_at": self.created_at,
"metadata": dict(self.metadata),
}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> "RunRecord":
"""从字典重建 run 记录。
Demo 输出:
`RunRecord(id='run-1a2b3c4d', task='生成周报', mode=<ExecutionMode.SWARMS: 'swarms'>, ...)`
"""
return cls(
id=str(payload.get("id") or new_record_id("run")),
task=str(payload.get("task") or ""),
mode=parse_execution_mode(payload.get("mode")),
success=bool(payload.get("success", False)),
summary=str(payload.get("summary") or ""),
error=str(payload["error"]) if payload.get("error") else None,
procedure_id=str(payload["procedure_id"]) if payload.get("procedure_id") else None,
created_at=str(payload.get("created_at") or now_iso()),
metadata=payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {},
)
@dataclass(slots=True)
class BridgeAttempt:
"""单次 bridge 执行尝试的归一化结果。
Demo 输出:
`BridgeAttempt(mode=<ExecutionMode.SWARMS: 'swarms'>, success=False, summary='执行失败', error='timeout', targets=['writer-agent'])`
"""
# 记录尝试来自哪个 bridge便于 swarms 链路审计。
mode: ExecutionMode
# 是否成功决定最终团队结果状态。
success: bool
# 本次尝试的聚合摘要。
summary: str
# 若失败,则记录错误原因。
error: str | None = None
# 保留成员级结果,供公告和测试直接读取。
member_results: list[AgentRunResult] = field(default_factory=list)
# 记录本次尝试的目标 agent。
targets: list[str] = field(default_factory=list)
# 透传底层调试字段。
raw: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
"""把单次尝试转成字典。
Demo 输出:
`{"mode": "swarms", "success": false, "targets": ["writer-agent"], ...}`
"""
return {
"mode": self.mode.value,
"success": self.success,
"summary": self.summary,
"error": self.error,
"member_results": [agent_result_to_dict(item) for item in self.member_results],
"targets": list(self.targets),
"raw": dict(self.raw),
}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> "BridgeAttempt":
"""从字典重建单次尝试。
Demo 输出:
`BridgeAttempt(mode=<ExecutionMode.SWARMS: 'swarms'>, success=True, summary='swarms 完成', ...)`
"""
return cls(
mode=parse_execution_mode(payload.get("mode")),
success=bool(payload.get("success", False)),
summary=str(payload.get("summary") or ""),
error=str(payload["error"]) if payload.get("error") else None,
member_results=[
agent_result_from_dict(item)
for item in payload.get("member_results", [])
if isinstance(item, dict)
],
targets=[str(item) for item in payload.get("targets", []) if str(item).strip()],
raw=payload.get("raw") if isinstance(payload.get("raw"), dict) else {},
)
@dataclass(slots=True)
class BridgeResult:
"""统一封装 `SwarmsBridge` 的最终输出。
Demo 输出:
`BridgeResult(mode=<ExecutionMode.SWARMS: 'swarms'>, success=True, summary='swarms 已完成', ...)`
"""
# 最终采用的执行模式。
mode: ExecutionMode
# 编排结果是否成功。
success: bool
# 最终可展示摘要。
summary: str
# 失败时的归一化错误说明。
error: str | None = None
# 当前结果对应的成员结果,一般取最终一次 attempt。
member_results: list[AgentRunResult] = field(default_factory=list)
# 探索阶段提炼出的候选 procedure。
candidate_procedure: ProcedureRecord | None = None
# 命中的历史 procedure便于公告和 run 记录追踪。
matched_procedure: ProcedureRecord | None = None
# 支持记录多次尝试,便于后续扩展到 swarms 内部多阶段路由。
attempts: list[BridgeAttempt] = field(default_factory=list)
# 原始调试字段统一放在这里。
raw: dict[str, Any] = field(default_factory=dict)
def last_member_results(self) -> list[AgentRunResult]:
"""返回最后一次有成员结果的 attempt。
Demo 输出:
`[AgentRunResult(agent_id='writer-agent', agent_name='Writer Agent', status='ok', summary='...', raw=None)]`
"""
# 优先使用显式写入的最终成员结果,避免每次都从 attempts 倒推。
if self.member_results:
return list(self.member_results)
# 若最终结果没显式写入,则从最后一个有成员结果的 attempt 回退。
for attempt in reversed(self.attempts):
if attempt.member_results:
return list(attempt.member_results)
return []
def to_dict(self) -> dict[str, Any]:
"""把 bridge 结果转成字典。
Demo 输出:
`{"mode": "exploration", "success": true, "attempts": [...], "candidate_procedure": {...}}`
"""
return {
"mode": self.mode.value,
"success": self.success,
"summary": self.summary,
"error": self.error,
"member_results": [agent_result_to_dict(item) for item in self.member_results],
"candidate_procedure": self.candidate_procedure.to_dict() if self.candidate_procedure else None,
"matched_procedure": self.matched_procedure.to_dict() if self.matched_procedure else None,
"attempts": [attempt.to_dict() for attempt in self.attempts],
"raw": dict(self.raw),
}
@classmethod
def from_dict(cls, payload: dict[str, Any]) -> "BridgeResult":
"""从字典重建 bridge 结果。
Demo 输出:
`BridgeResult(mode=<ExecutionMode.SWARMS: 'swarms'>, success=False, summary='执行失败', ...)`
"""
return cls(
mode=parse_execution_mode(payload.get("mode")),
success=bool(payload.get("success", False)),
summary=str(payload.get("summary") or ""),
error=str(payload["error"]) if payload.get("error") else None,
member_results=[
agent_result_from_dict(item)
for item in payload.get("member_results", [])
if isinstance(item, dict)
],
candidate_procedure=(
ProcedureRecord.from_dict(payload["candidate_procedure"])
if isinstance(payload.get("candidate_procedure"), dict)
else None
),
matched_procedure=(
ProcedureRecord.from_dict(payload["matched_procedure"])
if isinstance(payload.get("matched_procedure"), dict)
else None
),
attempts=[
BridgeAttempt.from_dict(item)
for item in payload.get("attempts", [])
if isinstance(item, dict)
],
raw=payload.get("raw") if isinstance(payload.get("raw"), dict) else {},
)