feat: 添加swarms团队编排功能并优化agent委派系统

- 引入AgentTeamOrchestrator支持多agent协同任务执行
- 增加第三方swarms库依赖并配置git协议替换以改善包管理
- 扩展DelegationManager支持团队任务调度和进度跟踪
- 实现中文bigram分词算法提升中文任务检索准确性
- 调整A2AClient和DelegationManager超时时间从30秒增至600秒
- 优化AgentRunResult状态判断逻辑增加有意义摘要检测
- 修改Dockerfile配置npm仓库镜像地址和git协议映射
- 更新CLI命令行接口支持网关端口配置传递
- 调整提供者超时配置机制增强请求稳定性
- 移除过时的support_group字段简化agent描述符结构
- 增强错误处理和进度事件报告机制改进用户体验
This commit is contained in:
2026-04-14 14:34:23 +08:00
parent fee9007da6
commit cdfc222c9f
85 changed files with 5443 additions and 1392 deletions

View File

@ -28,6 +28,8 @@ from nanobot.agent.process_events import (
process_run_context,
)
from nanobot.agent.run_result import AgentRunResult
from nanobot.agent_team.orchestrator import AgentTeamOrchestrator
from nanobot.agent_team.types import BridgeResult
from nanobot.bus.events import InboundMessage, OutboundMessage
from nanobot.bus.queue import MessageBus
from nanobot.providers.base import LLMProvider
@ -61,12 +63,13 @@ class DelegationManager:
def __init__(
self,
provider: LLMProvider,
model: str | None,
workspace: Path,
bus: MessageBus,
registry: AgentRegistry,
skills_loader: "SkillsLoader | None",
local_executor: Any,
timeout_seconds: int = 30,
timeout_seconds: int = 600,
poll_interval_seconds: int = 2,
card_cache_ttl_seconds: int = 300,
max_parallel_agents: int = 4,
@ -76,6 +79,7 @@ class DelegationManager:
allow_local_delegation: bool = True,
allow_plugin_delegation: bool = True,
allow_local_fallback: bool = True,
gateway_port: int = 18790,
):
self.provider = provider
self.workspace = workspace
@ -97,6 +101,18 @@ class DelegationManager:
authz_config=authz_config,
backend_identity=backend_identity,
)
# 新 orchestrator 只负责 agent team 路径;单 agent 委派仍走原有逻辑。
self.agent_team_orchestrator = AgentTeamOrchestrator(
workspace=workspace,
provider=provider,
model=model,
registry=registry,
bus=bus,
local_executor=local_executor,
member_runner=self._run_team_member_for_swarms,
max_parallel_agents=self.max_parallel_agents,
gateway_port=gateway_port,
)
self._running_tasks: dict[str, DelegationRun] = {}
self._direct_announcement_callback: DirectAnnouncementCallback | None = None
@ -273,6 +289,20 @@ class DelegationManager:
"""返回当前正在执行的委派数量。"""
return len(self._running_tasks)
@staticmethod
def _clean_metadata(metadata: dict[str, Any]) -> dict[str, Any]:
"""删除空值,避免过程事件 metadata 出现大量噪声字段。"""
cleaned: dict[str, Any] = {}
for key, value in metadata.items():
if value is None:
continue
if isinstance(value, str) and not value.strip():
continue
if isinstance(value, (list, tuple, set, dict)) and not value:
continue
cleaned[key] = value
return cleaned
@staticmethod
def _ui_status(status: str | None) -> str:
"""把底层状态归一化成前端更稳定的显示状态。"""
@ -287,6 +317,29 @@ class DelegationManager:
return "error"
return probe or "running"
async def _emit_team_progress(
self,
run_id: str,
text: str,
*,
stage_label: str,
metadata: dict[str, Any] | None = None,
) -> None:
"""为 agent team 根 run 发一条过程可观察事件。"""
await emit_process_event(
"process_run_progress",
run_id=run_id,
actor_type="system",
actor_id="agent-group",
actor_name="Agent Team",
text=text,
metadata=self._clean_metadata({
"source": "agent_team_dispatch",
"stage_label": stage_label,
**(metadata or {}),
}),
)
async def _emit_agent_started(
self,
run_id: str,
@ -310,7 +363,6 @@ class DelegationManager:
metadata={
"kind": descriptor.kind,
"protocol": descriptor.protocol,
"support_group": descriptor.support_group,
"support_streaming": descriptor.support_streaming,
"delegated_task": task,
},
@ -371,31 +423,56 @@ class DelegationManager:
actor_type="system",
actor_id="agent-group",
actor_name="Agent Team",
source="agent_team",
title=label,
status="running",
metadata={"targets": targets},
metadata=self._clean_metadata({
"source": "agent_team_dispatch",
"phase": "dispatch",
"stage_label": "团队任务已创建",
"planned_targets": targets,
"selected_targets": targets,
"selected_count": len(targets),
}),
)
async def _emit_group_finished(self, run_id: str, label: str, results: list[AgentRunResult]) -> None:
"""发送 agent team 结束事件。"""
async def _emit_group_finished(
self,
run_id: str,
label: str,
results: list[AgentRunResult],
*,
status: str = "done",
summary: str | None = None,
metadata_extra: dict[str, Any] | None = None,
) -> None:
"""发送 agent team 结束事件。
Demo 输出:
`process_run_finished(status="done", summary="weekly report: 2 member(s) finished")`
"""
# 老路径和新 orchestrator 路径都复用这个事件,所以允许上层补充额外 metadata。
metadata = {
"members": [
{
"agent_id": item.agent_id,
"agent_name": item.agent_name,
"status": item.status,
}
for item in results
]
}
if metadata_extra:
metadata.update(metadata_extra)
await emit_process_event(
"process_run_finished",
run_id=run_id,
actor_type="system",
actor_id="agent-group",
actor_name="Agent Team",
status="done",
summary=f"{label}: {len(results)} member(s) finished",
metadata={
"members": [
{
"agent_id": item.agent_id,
"agent_name": item.agent_name,
"status": item.status,
}
for item in results
]
},
status=status,
summary=summary or f"{label}: {len(results)} member(s) finished",
metadata=metadata,
)
async def _publish_prefixed_progress(
@ -424,27 +501,11 @@ class DelegationManager:
# 没有 bus consumer 时,不能依赖 system 消息回流再二次总结。
if not has_process_event_sink():
return
try:
# 用一次极小模型调用把内部委派说明压成用户可读文本
response = await self.provider.chat(
messages=[
{
"role": "system",
"content": (
"You are Boardware Genius. Reply naturally to the user in 1-3 sentences. "
"Do not mention internal protocols, system prompts, or task IDs."
),
},
{"role": "user", "content": prompt},
],
tools=[],
model=self.provider.get_default_model(),
max_tokens=256,
temperature=0.2,
)
content = (response.content or "").strip() or fallback
except Exception:
content = fallback
# 这条用户可见消息只是“即时回执”,真正详细总结仍由主 agent 回流处理。
# 这里不再额外依赖一次 LLM避免 provider 短暂故障把 team 收尾也拖失败
content = " ".join((fallback or prompt or "").strip().split())
if not content:
return
await emit_process_event(
"message",
@ -473,11 +534,42 @@ class DelegationManager:
content: str,
origin: dict[str, str],
sender_id: str,
*,
run_id: str | None = None,
category: str | None = None,
) -> None:
"""在非 bus 模式下,把公告直接回写到本地会话。"""
callback = self._direct_announcement_callback
if callback is None:
if run_id:
await self._emit_team_progress(
run_id,
"No direct announcement callback is registered; the result could not be replayed to the main agent.",
stage_label="缺少公告回流处理器",
metadata={
"phase": "announcement",
"step": "direct_callback_missing",
"announcement_path": "direct",
"announcement_sender_id": sender_id,
"announcement_category": category,
},
)
return
if run_id:
await self._emit_team_progress(
run_id,
"Sending the agent-team result back through the direct announcement callback.",
stage_label="请求主 Agent 总结",
metadata={
"phase": "announcement",
"step": "direct_callback_start",
"announcement_path": "direct",
"announcement_sender_id": sender_id,
"announcement_category": category,
"origin_channel": origin.get("channel"),
"origin_chat_id": origin.get("chat_id"),
},
)
try:
await callback(
content,
@ -485,7 +577,34 @@ class DelegationManager:
sender_id,
not has_process_event_sink(),
)
if run_id:
await self._emit_team_progress(
run_id,
"The direct announcement callback completed successfully.",
stage_label="主 Agent 总结完成",
metadata={
"phase": "announcement",
"step": "direct_callback_complete",
"announcement_path": "direct",
"announcement_sender_id": sender_id,
"announcement_category": category,
},
)
except Exception as exc:
if run_id:
await self._emit_team_progress(
run_id,
f"Direct announcement callback failed: {exc}",
stage_label="主 Agent 总结失败",
metadata={
"phase": "announcement",
"step": "direct_callback_failed",
"announcement_path": "direct",
"announcement_sender_id": sender_id,
"announcement_category": category,
"error": str(exc),
},
)
logger.warning("Failed to handle direct delegation announcement: {}", exc)
async def _run_dispatch(
@ -510,23 +629,80 @@ class DelegationManager:
if is_group:
planned_targets = list(targets)
await self._emit_group_started(run_id, label, planned_targets)
results = await self._run_group(
task,
label,
None,
targets,
strategy,
skills,
origin=origin,
run_id=run_id,
announce_via_bus=announce_via_bus,
await self._emit_team_progress(
run_id,
"Agent team dispatch accepted and moved into swarms orchestration.",
stage_label="开始团队编排",
metadata={
"phase": "dispatch",
"strategy": strategy,
"execution_path": "swarms",
"announce_via_bus": announce_via_bus,
"requested_targets": planned_targets,
},
)
await self._emit_group_finished(run_id, label, results)
await self._announce_group_result(
logger.info(
"Agent team [{}] dispatch started: mode=swarms announce_via_bus={} requested_targets={}",
run_id,
announce_via_bus,
planned_targets,
)
await self._emit_team_progress(
run_id,
"DelegationManager handed the task to AgentTeamOrchestrator.",
stage_label="编排器接管任务",
metadata={
"phase": "orchestrator",
"step": "handoff_to_orchestrator",
"requested_targets": planned_targets,
},
)
orchestrated = await self.agent_team_orchestrator.run_task(
task=task,
label=label,
skills=skills,
origin=origin,
announce_via_bus=announce_via_bus,
run_id=run_id,
)
await self._emit_team_progress(
run_id,
"AgentTeamOrchestrator returned a final bridge result.",
stage_label="编排器已返回结果",
metadata={
"phase": "orchestrator",
"step": "orchestrator_result_ready",
"execution_mode": orchestrated.mode.value,
"candidate_procedure_id": (
orchestrated.candidate_procedure.id
if orchestrated.candidate_procedure is not None
else None
),
"attempt_count": len(orchestrated.attempts),
"success": orchestrated.success,
},
)
await self._emit_group_finished(
run_id,
label,
orchestrated.last_member_results(),
status="done" if orchestrated.success else "error",
summary=orchestrated.summary,
metadata_extra={
"execution_mode": orchestrated.mode.value,
"candidate_procedure_id": (
orchestrated.candidate_procedure.id
if orchestrated.candidate_procedure is not None
else None
),
"attempts": [attempt.to_dict() for attempt in orchestrated.attempts],
},
)
await self._announce_orchestrator_result(
run_id,
label,
task,
results,
orchestrated,
origin,
announce_via_bus=announce_via_bus,
)
@ -591,6 +767,16 @@ class DelegationManager:
summary=f"Error: {exc}",
)
if is_group:
await self._emit_team_progress(
run_id,
f"Agent team execution failed before announcement: {exc}",
stage_label="团队执行失败",
metadata={
"phase": "error",
"step": "dispatch_failed",
"error": str(exc),
},
)
await emit_process_event(
"process_run_finished",
run_id=run_id,
@ -777,94 +963,57 @@ class DelegationManager:
and ("subagent" in lowered or "sub-agent" in lowered)
)
async def _run_group(
async def _run_team_member_for_swarms(
self,
descriptor: AgentDescriptor,
task: str,
label: str,
target: str | None,
targets: list[str],
strategy: str,
parent_run_id: str,
skills: list[str],
origin: dict[str, str],
run_id: str,
announce_via_bus: bool,
) -> list[AgentRunResult]:
"""并行执行一组 agent并汇总结果。"""
resolved_targets = list(targets)
if target:
resolved_targets.append(target)
if not resolved_targets:
# 未显式给出目标时,根据任务文本自动挑若干个候选 agent。
suggestions = [
agent for agent in self.registry.suggest_agents(task, limit=self.max_parallel_agents * 2)
if self._descriptor_allowed(agent)
]
resolved_targets = [agent.id for agent in suggestions]
if not resolved_targets:
descriptor = self.registry.get_agent("local-subagent")
if descriptor and self._descriptor_allowed(descriptor):
resolved_targets = [descriptor.id]
if not resolved_targets:
raise ValueError("No agents available for group delegation")
resolved_targets = list(dict.fromkeys(resolved_targets))
descriptors: list[AgentDescriptor] = []
missing: list[str] = []
for item in resolved_targets:
descriptor = self.registry.get_agent(item)
if descriptor is None:
missing.append(item)
else:
self._ensure_descriptor_allowed(descriptor)
descriptors.append(descriptor)
if missing:
raise ValueError(f"Agent(s) not found: {', '.join(missing)}")
semaphore = asyncio.Semaphore(self.max_parallel_agents)
async def _run_one(descriptor: AgentDescriptor) -> AgentRunResult:
# group 内每个成员都分配独立 child run_id便于前端区分子树。
child_run_id = new_run_id("agent")
async with semaphore:
try:
await self._emit_agent_started(
child_run_id,
descriptor,
label,
parent_run_id=run_id,
task=task,
)
result = await self._execute_descriptor(
descriptor,
task,
label,
skill_names=skills,
event_callback=self._build_progress_callback(
origin,
descriptor,
event_run_id=child_run_id,
tracking_run_id=run_id,
publish_via_bus=announce_via_bus,
),
task_callback=self._build_task_callback(run_id, descriptor),
process_run_id=child_run_id,
)
await self._emit_agent_finished(child_run_id, descriptor, result)
return result
except asyncio.CancelledError:
await self._emit_agent_cancelled(child_run_id, descriptor, label)
raise
except Exception as exc:
result = AgentRunResult(
agent_id=descriptor.id,
agent_name=descriptor.name,
status="error",
summary=f"Error: {exc}",
)
await self._emit_agent_finished(child_run_id, descriptor, result)
return result
results = await asyncio.gather(*[_run_one(agent) for agent in descriptors])
return results
) -> AgentRunResult:
"""Execute one swarms-selected nanobot agent as a process child run."""
state = self._running_tasks.get(parent_run_id)
label = "Agent Team" if state is None else state.label
origin = {"channel": "system", "chat_id": "direct"} if state is None else state.origin
announce_via_bus = True if state is None else state.announce_via_bus
child_run_id = new_run_id("agent")
try:
self._ensure_descriptor_allowed(descriptor)
await self._emit_agent_started(
child_run_id,
descriptor,
label,
parent_run_id=parent_run_id,
task=task,
)
result = await self._execute_descriptor(
descriptor,
task,
label,
skill_names=skills,
event_callback=self._build_progress_callback(
origin,
descriptor,
event_run_id=child_run_id,
tracking_run_id=parent_run_id,
publish_via_bus=announce_via_bus,
),
task_callback=self._build_task_callback(parent_run_id, descriptor),
process_run_id=child_run_id,
)
await self._emit_agent_finished(child_run_id, descriptor, result)
return result
except asyncio.CancelledError:
await self._emit_agent_cancelled(child_run_id, descriptor, label)
raise
except Exception as exc:
result = AgentRunResult(
agent_id=descriptor.id,
agent_name=descriptor.name,
status="error",
summary=f"Error: {exc}",
)
await self._emit_agent_finished(child_run_id, descriptor, result)
return result
async def _execute_descriptor(
self,
@ -1164,52 +1313,102 @@ class DelegationManager:
)
logger.debug("Delegation [{}] announced result", run_id)
async def _announce_group_result(
async def _announce_orchestrator_result(
self,
run_id: str,
label: str,
task: str,
results: list[AgentRunResult],
result: BridgeResult,
origin: dict[str, str],
*,
announce_via_bus: bool,
) -> None:
"""公告 agent team 汇总结果。"""
lines = [f"[Agent team '{label}' completed]", "", f"Task: {task}", "", "Members:"]
for result in results:
lines.append(f"- {result.agent_name} ({result.agent_id}): {result.status}")
lines.extend(["", "Results:"])
for result in results:
lines.append(f"### {result.agent_name} ({result.status})")
lines.append(result.summary)
lines.append("")
lines.append(
"Summarize this naturally for the user. Mention disagreements or failures if any."
"""公告 orchestrator 驱动的 agent team 结果。
Demo 输出:
`[Agent team 'weekly report' completed]\nExecution mode: swarms\nMatched procedure: procedure-a1b2c3d4`
"""
# 这里显式保留 mode / procedure 信息,方便主 agent 做更准确的用户总结。
await self._emit_team_progress(
run_id,
"Preparing orchestrated agent-team summary for the main agent.",
stage_label="整理团队结果",
metadata={
"phase": "announcement",
"step": "build_orchestrator_summary",
"execution_mode": result.mode.value,
"attempt_count": len(result.attempts),
},
)
status_text = "completed" if result.success else "failed"
lines = [
f"[Agent team '{label}' {status_text}]",
"",
f"Task: {task}",
f"Execution mode: {result.mode.value}",
]
if result.matched_procedure is not None:
lines.append(
"Matched procedure: "
f"{result.matched_procedure.id} "
f"(confidence={result.matched_procedure.confidence:.2f})"
)
if result.attempts:
lines.extend(["", "Attempts:"])
for attempt in result.attempts:
attempt_status = "ok" if attempt.success else "error"
lines.append(f"- {attempt.mode.value}: {attempt_status}")
if attempt.error:
lines.append(f" error: {attempt.error}")
member_results = result.last_member_results()
if member_results:
lines.extend(["", "Members:"])
for item in member_results:
lines.append(f"- {item.agent_name} ({item.agent_id}): {item.status}")
lines.extend(["", "Results:"])
for item in member_results:
lines.append(f"### {item.agent_name} ({item.status})")
lines.append(item.summary)
lines.append("")
lines.extend([
"Final summary:",
result.summary,
"",
"Summarize this naturally for the user. Mention disagreements or failures if any.",
])
summary = "\n".join(lines).strip()
if announce_via_bus:
await self._publish_announcement(
summary,
origin,
sender_id="delegation-group",
sender_id="delegation-team",
run_id=run_id,
category="agent_team_orchestrated",
)
else:
await self._notify_direct_announcement(
summary,
origin,
"delegation-group",
"delegation-team",
run_id=run_id,
category="agent_team_orchestrated",
)
await self._emit_direct_user_message(
summary,
"Agent team 已完成,请查看各 agent 的结果与最终结论。",
"Agent team 已完成,请查看最终结论与各次尝试摘要",
)
logger.debug("Agent team [{}] announced result", run_id)
logger.debug("Agent team [{}] announced orchestrated result", run_id)
async def _publish_announcement(
self,
content: str,
origin: dict[str, str],
sender_id: str,
*,
run_id: str | None = None,
category: str | None = None,
) -> None:
"""通过 system inbound 消息把公告重新送回主 agent 链路。"""
msg = InboundMessage(
@ -1219,3 +1418,18 @@ class DelegationManager:
content=content,
)
await self.bus.publish_inbound(msg)
if run_id:
await self._emit_team_progress(
run_id,
"Team summary has been published back to the main agent via the system bus.",
stage_label="团队结果已回流",
metadata={
"phase": "announcement",
"step": "bus_publish_complete",
"announcement_path": "bus",
"announcement_sender_id": sender_id,
"announcement_category": category,
"origin_channel": origin.get("channel"),
"origin_chat_id": origin.get("chat_id"),
},
)