# A2A Multi-Agent 改造方案 ## 1. 需求目标 当前 `spawn`/`sub-agent` 只有一种执行方式: 创建一个本地后台 subagent 去完成任务。 这次需求要改成: 1. 调用 `sub-agent` 时,不一定新建本地 subagent。 2. 先从“已添加的 Agent”里找可用目标。 3. 再从 skills 中声明的 `agent cards` 里找可用目标。 4. 通过 A2A 协议把任务发给对应 agent。 5. 支持一个任务发给多个 agent,形成 `agent group`,最后回到主 agent 汇总。 6. 保持现有 `spawn(task, label)` 兼容,不破坏已有行为。 结论先说: - 最合适的做法不是继续把能力堆进 `SubagentManager`。 - 应该把“本地 subagent 执行”升级为“统一委派层”。 - `spawn` 工具继续保留,但语义从“创建 subagent”扩展为“委派给合适的 agent / agent group”。 ## 2. 当前代码现状 ### 2.1 当前触发链路 现有链路很单一: 1. `AgentLoop` 初始化 `SubagentManager` - 位置: `nanobot/agent/loop.py:88-114` 2. `AgentLoop._register_default_tools()` 注册 `SpawnTool` - 位置: `nanobot/agent/loop.py:116-138` 3. LLM 调用 `spawn(task, label)` 4. `SpawnTool.execute()` 直接转发给 `SubagentManager.spawn()` - 位置: `nanobot/agent/tools/spawn.py:67-76` 5. `SubagentManager.spawn()` 创建本地 asyncio 后台任务 - 位置: `nanobot/agent/subagent.py:64-93` 6. `_run_subagent()` 用一个受限工具集运行本地子代理 - 位置: `nanobot/agent/subagent.py:95-195` 7. `_announce_result()` 把结果包装成 `channel="system"` 的消息回投主消息总线 - 位置: `nanobot/agent/subagent.py:197-230` 8. `AgentLoop._process_message()` 接到 `system` 消息,再整理成用户可见回复 - 位置: `nanobot/agent/loop.py:331-347` ### 2.2 当前已经有但没接入调度链路的能力 仓库里已经有两类“候选 agent 信息”,但没有进入实际调度: 1. Plugin agents - `PluginLoader.find_agent()` 已能找 agent - 位置: `nanobot/agent/plugins.py:83-91` - `build_agents_summary()` 也已能汇总 agent 信息 - 位置: `nanobot/agent/plugins.py:100-121` - 但当前 `AgentLoop` / `ContextBuilder` 并没有用它做调度 2. Skills - `SkillsLoader` 已能枚举 / 读取 skill - 位置: `nanobot/agent/skills.py:32-249` - 但 skill 目前只被当作 prompt 资源,不会暴露成“可路由 agent” ### 2.3 当前缺口 当前缺少这几层: 1. 统一的 `Agent Registry` 2. A2A `agent card` 发现与缓存 3. A2A client 调用层 4. 统一的委派器,负责在“本地 subagent / plugin agent / skill agent card / agent group”之间做路由 5. group 级别的状态管理和结果聚合 ## 3. 推荐总方案 推荐采用“保留 `spawn` 工具名,重构内部执行层”的方案。 ### 3.1 核心思路 把当前: - `SpawnTool -> SubagentManager -> 本地 subagent` 改成: - `SpawnTool -> DelegationManager -> AgentResolver -> Executor(local/plugin/a2a/group)` 也就是: 1. `spawn` 不再等价于“必须创建 subagent”。 2. `spawn` 变成“委派任务”。 3. 真正执行方式由委派层动态决定。 ### 3.2 为什么这样最合适 如果直接继续扩 `SubagentManager`,很快会出现这些问题: 1. 一个类同时负责本地 LLM 运行、A2A 网络调用、agent card 发现、group 并发、结果聚合。 2. 后续要支持 plugin agent、本地 named agent、A2A streaming 时会越来越乱。 3. 当前 `SubagentManager` 的职责本来就已经比较明确: “本地后台 subagent 执行器”。 所以更合理的拆法是: 1. `SubagentManager` 保留或下沉为 `LocalSubagentExecutor` 2. 新增 `DelegationManager` 作为统一入口 3. 新增 `AgentRegistry` / `AgentResolver` 4. 新增 `A2AClient` ## 4. 推荐模块拆分 ### 4.1 新增 `DelegationManager` 建议新文件: - `nanobot/agent/delegation.py` 职责: 1. 接收 `spawn` 请求 2. 根据参数和任务内容选择目标 agent 3. 决定执行方式 4. 对 group 做并发调度 5. 统一把结果回投主消息总线 建议接口: ```python class DelegationManager: async def dispatch( self, task: str, label: str | None = None, target: str | None = None, targets: list[str] | None = None, strategy: str = "auto", origin_channel: str = "cli", origin_chat_id: str = "direct", ) -> str: ... ``` ### 4.2 保留本地执行器 当前 `nanobot/agent/subagent.py` 的 `_run_subagent()` 逻辑可以保留,但角色改为: - `LocalSubagentExecutor` 也可以第一版不重命名文件,只把里面逻辑拆成: 1. `spawn_local()` 2. `_run_local_subagent()` 3. `_announce_local_result()` 这样可以最小改动落地。 ### 4.3 新增 `AgentRegistry` 建议新文件: - `nanobot/agent/agent_registry.py` 职责: 1. 汇总所有可调度 agent 2. 统一输出规范化 descriptor 3. 维护优先级和去重逻辑 统一后的 agent 来源: 1. workspace 中“已添加的 agent” 2. plugin agents 3. skill frontmatter 里声明的 `agent_cards` 4. 必要时 fallback 到本地 `local-subagent` 建议统一 descriptor: ```python @dataclass class AgentDescriptor: id: str name: str description: str source: str # workspace | plugin | skill | builtin kind: str # local_prompt | a2a_remote | local_fallback protocol: str | None # a2a | None plugin_name: str | None = None skill_name: str | None = None model: str | None = None endpoint: str | None = None card_url: str | None = None tags: list[str] = field(default_factory=list) capabilities: dict[str, Any] = field(default_factory=dict) metadata: dict[str, Any] = field(default_factory=dict) ``` ### 4.4 新增 A2A client 层 建议新目录: - `nanobot/a2a/client.py` - `nanobot/a2a/cards.py` - `nanobot/a2a/models.py` 职责: 1. 获取 agent card 2. 解析 card 能力 3. 对远端 agent 发 JSON-RPC 请求 4. 处理同步返回 / task 轮询 / streaming 兼容 ## 5. 代码插入点 ## 5.1 `nanobot/agent/loop.py` ### 插入点 A: `__init__` 当前: - `self.subagents = SubagentManager(...)` - 位置: `nanobot/agent/loop.py:88-102` 建议改成: 1. 初始化 `PluginLoader` 2. 初始化 `AgentRegistry` 3. 初始化 `DelegationManager` 4. `DelegationManager` 内部持有 `LocalSubagentExecutor` / `A2AExecutor` 推荐形态: ```python self.plugins = PluginLoader(workspace) self.agent_registry = AgentRegistry(workspace, plugins=self.plugins, ...) self.delegation = DelegationManager( provider=provider, workspace=workspace, bus=bus, registry=self.agent_registry, ... ) ``` ### 插入点 B: `_register_default_tools` 当前: - 注册 `SpawnTool(manager=self.subagents)` - 位置: `nanobot/agent/loop.py:130-134` 建议改成: ```python self.tools.register(SpawnTool(manager=self.delegation)) ``` ### 插入点 C: `_set_tool_context` 当前会给 `spawn` 工具写 origin context: - 位置: `nanobot/agent/loop.py:165-192` 这里逻辑可以继续保留,不需要大改,因为 A2A / group 结果最终也要回到原会话。 ## 5.2 `nanobot/agent/tools/spawn.py` 当前 `SpawnTool` 参数只有: - `task` - `label` 位置: - schema: `nanobot/agent/tools/spawn.py:49-65` - execute: `nanobot/agent/tools/spawn.py:67-76` 建议扩成: ```python { "task": "string", "label": "string?", "target": "string?", "targets": "string[]?", "strategy": "auto|local|plugin|a2a|group" } ``` 兼容规则: 1. 老调用只传 `task/label` 时,等价于 `strategy="auto"` 2. `target` 表示单目标 3. `targets` 表示 group 4. `strategy="local"` 强制走本地 subagent 5. `strategy="a2a"` 强制只找 A2A 目标 ## 5.3 `nanobot/agent/context.py` 当前 prompt 中只注入: 1. bootstrap 2. memory 3. skills summary 位置: - `build_system_prompt()`: `nanobot/agent/context.py:38-76` 建议新增一段: - `# Available Agents` 由 `AgentRegistry.build_agents_summary()` 生成,内容只放: 1. agent id / name 2. 简短 description 3. source 4. protocol 5. 是否支持 group / streaming 目标是让主 agent 知道: 1. 当前有哪些现成 agent 可用 2. 什么时候应该 `spawn(target=...)` 3. 哪些是 skill 暴露出来的 A2A agent ## 5.4 `nanobot/agent/skills.py` 这是 skill agent cards 的关键入口。 当前 skill frontmatter 已支持 `metadata` 字段,并会解析其中的 JSON: - `_parse_nanobot_metadata()`: `nanobot/agent/skills.py:190-196` - `_get_skill_meta()`: `nanobot/agent/skills.py:209-212` 最推荐的做法不是去扫 `SKILL.md` 正文里的自由文本,而是约定 skill frontmatter 的 `metadata.nanobot.agent_cards`。 建议新增: ```python def list_skill_agent_cards(self) -> list[dict[str, Any]]: ... ``` 推荐 skill 写法: ```md --- name: github-research description: GitHub research helper metadata: '{"nanobot":{"agent_cards":[{"id":"repo-analyst","url":"https://example.com/.well-known/agent-card","tags":["github","research"],"auth_env":"REPO_AGENT_TOKEN"}]}}' --- ``` 为什么推荐这样做: 1. 当前 frontmatter 解析已经存在 2. 不需要引入新的 skill 文件格式 3. 不需要解析自由文本 4. skill 打包/上传链路也不需要大改 ## 5.5 `nanobot/agent/plugins.py` 当前 plugin agents 已能加载: - `find_agent()`: `nanobot/agent/plugins.py:83-91` - `_load_agents()`: `nanobot/agent/plugins.py:210-229` 建议: 1. `AgentRegistry` 直接复用 `PluginLoader` 2. plugin agent 作为“本地可执行 agent”来源之一 这里不建议把 plugin agent 强行转成 A2A。 更合理的处理是: 1. plugin agent 本地执行 2. skill agent cards 远程 A2A 调用 3. workspace 手动添加的 agent 也可走 A2A ## 5.6 `nanobot/config/schema.py` 当前 `ToolsConfig` 只有: - `web` - `exec` - `restrict_to_workspace` - `mcp_servers` 位置: - `nanobot/config/schema.py:337-347` 建议新增: ```python class A2AConfig(Base): enabled: bool = True timeout_seconds: int = 30 poll_interval_seconds: int = 2 card_cache_ttl_seconds: int = 300 max_parallel_agents: int = 4 allow_skill_cards: bool = True allow_workspace_agents: bool = True allowed_hosts: list[str] = Field(default_factory=list) ``` 然后挂到: ```python class ToolsConfig(Base): ... a2a: A2AConfig = Field(default_factory=A2AConfig) ``` ## 5.7 `nanobot/web/server.py` 当前 web API 有: - `/api/skills` - `/api/plugins` 位置: - skills: `nanobot/web/server.py:702-843` - plugins: `nanobot/web/server.py:1000-1037` 建议新增: 1. `GET /api/agents` - 返回统一后的 agent registry 2. `POST /api/agents` - 添加 workspace agent card 3. `DELETE /api/agents/{id}` - 删除 workspace agent 4. `POST /api/agents/refresh` - 刷新 card cache 这样“已添加的 Agent”才有明确的持久化来源。 ## 6. 推荐的数据来源优先级 为了行为稳定,推荐 resolver 按以下优先级匹配: 1. workspace 手动添加的 agent 2. plugin agents 3. skill metadata 里的 agent cards 4. fallback 到本地 subagent 原因: 1. workspace 手动添加通常是用户明确希望接入的 agent 2. plugin agent 是本地稳定能力 3. skill card 往往是外部资源,可信度和可用性最弱 4. 本地 subagent 最后兜底,保证老行为不失效 ## 7. A2A 协议接入建议 ## 7.1 Agent Card 发现 建议支持 3 种入口: 1. 显式 `card_url` 2. `base_url + /.well-known/agent-card` 3. fallback `base_url + /.well-known/agent.json` 这样做的原因是: 1. 当前 A2A 文档和样例在 card 路径上存在新旧写法并存 2. 兼容性会更好 ## 7.2 RPC 调用兼容层 建议客户端优先尝试: 1. `tasks/send` 2. 不支持时 fallback `message/send` 后续可选支持: 1. `tasks/sendSubscribe` 2. `message/sendStream` 3. `tasks/get` 4. `tasks/cancel` 推荐第一期先做: 1. 非流式发任务 2. 如果返回 `Task` 状态不是最终态,就轮询 `tasks/get` 这样能最小代价先打通。 ## 7.3 发送给远端 agent 的上下文范围 不要把主会话完整 history 直接发给远端 agent。 建议第一版只发送: 1. 任务目标 2. 必要的结构化说明 3. 主 agent 整理好的最小上下文 原因: 1. 当前本地 subagent 也不共享主会话历史 2. 外部 A2A agent 不可信时,最小化数据泄漏面 3. 避免 token 膨胀 ## 8. agent group 设计 ## 8.1 什么时候触发 group 建议第一版只支持两种触发: 1. 用户明确指定多个 agent 2. LLM 在工具调用里显式传 `targets=[...]` 不建议第一版做“自动拆成多个 agent 并行”的强自动化。 原因: 1. 容易失控 2. 很难解释为什么调了这些 agent 3. 对成本和网络调用不可控 ## 8.2 group 执行链路 推荐链路: 1. `SpawnTool.execute()` 收到 `targets` 2. `DelegationManager.dispatch()` 创建 `group_run_id` 3. `AgentRegistry` 解析出每个 target 的 descriptor 4. 按 executor 类型并发执行 5. `asyncio.gather(..., return_exceptions=True)` 收集结果 6. 统一做 group aggregation 7. `_announce_group_result()` 回投主消息总线 8. 主 agent 再生成最终用户回复 ## 8.3 group 结果聚合 建议 group 执行器输出结构化结果: ```python @dataclass class AgentRunResult: agent_id: str status: str # ok | error | timeout | cancelled summary: str raw: dict[str, Any] | None = None ``` group 最终回投内容建议类似: ```text [Agent group 'repo-check' completed] Members: - researcher: ok - reviewer: ok - planner: error Results: ... Summarize this naturally for the user. Mention disagreements if any. ``` 这样能继续复用当前 `system -> main agent -> user` 的输出模式。 ## 9. 推荐触发方式 ## 9.1 用户显式触发 用户说法示例: 1. “把这个任务交给 `github-reviewer`” 2. “让 `researcher` 和 `reviewer` 一起处理” 3. “如果有现成 agent 就不要新建 subagent” 这时主 agent 应调用: ```json { "task": "...", "target": "github-reviewer" } ``` 或者: ```json { "task": "...", "targets": ["researcher", "reviewer"], "strategy": "group" } ``` ## 9.2 模型自主触发 当主 agent 判断: 1. 任务独立可并行 2. 已有 agent 专长明显更匹配 3. 任务耗时长,适合后台执行 则调用 `spawn`,但不再默认认为一定是“新建本地 subagent”。 ## 9.3 自动回退 如果没有找到匹配 agent: 1. `strategy=auto` -> fallback 本地 subagent 2. `strategy=a2a` -> 直接返回未找到 3. `strategy=group` 且部分目标不存在 -> 明确报错或只跑已解析目标,建议第一版严格报错 ## 10. workspace 中“已添加 agent”的建议存储 建议新增: - `workspace/agents/registry.json` 示例: ```json [ { "id": "github-reviewer", "name": "GitHub Reviewer", "description": "Review GitHub repository changes", "protocol": "a2a", "base_url": "https://reviewer.example.com/a2a", "card_url": "https://reviewer.example.com/.well-known/agent-card", "auth_env": "GITHUB_REVIEWER_TOKEN", "enabled": true, "tags": ["github", "review"] } ] ``` 为什么不用直接塞进 `config.json`: 1. 这是 workspace 维度资源,不是全局运行参数 2. web API 做增删改查更方便 3. 不要求用户每次改 agent 都改配置再重启 ## 11. 推荐实施顺序 ### Phase 1: 打通单 agent 路由 目标: 1. 引入 `AgentRegistry` 2. `spawn` 支持 `target` 3. 支持 workspace agent 和 skill agent card 4. 支持 A2A 单点调用 5. 找不到时 fallback 本地 subagent ### Phase 2: 接入 plugin agent 本地执行 目标: 1. plugin agent 进入统一 registry 2. plugin agent 可作为 `target` 3. 本地 prompt-based agent 与 A2A remote agent 共存 ### Phase 3: group 并发和聚合 目标: 1. `targets=[...]` 2. 并发执行 3. group 级状态跟踪 4. 聚合后回投主 agent ### Phase 4: web 管理接口 目标: 1. `/api/agents` 2. 添加 / 删除 / 刷新 agent 3. 前端展示 unified registry ## 12. 兼容性要求 这次改造一定要保留以下兼容性: 1. 旧的 `spawn(task, label)` 调用仍然可用 2. 没有 A2A agent 时,行为和现在一致 3. skill 没写 `agent_cards` 时,skill 仍只是普通 skill 4. plugin agent 不参与调度时,现有 plugin 机制不受影响 ## 13. 风险点 ### 13.1 A2A 规范新旧写法并存 从当前公开文档和样例看,存在这些并行写法: 1. card 路径: `/.well-known/agent-card` 和 `/.well-known/agent.json` 2. RPC 方法: `tasks/send` 和 `message/send` 所以客户端必须做兼容适配,不能写死一种。 ### 13.2 外部 agent 的安全边界 需要限制: 1. 白名单 host 2. 超时 3. card cache TTL 4. skill card 是否允许自动启用 ### 13.3 远端 agent 无法直接访问本地 workspace 这意味着: 1. 不能把“去读本地文件然后处理”原样发给远端 A2A agent 2. 主 agent 需要先整理出必要上下文 3. 第一版最好只做文本级委派 ## 14. 我建议的落地结论 如果要控制改动面,又要保证后续可扩展,推荐最终采用下面这个结构: ```text AgentLoop -> SpawnTool -> DelegationManager -> AgentRegistry / AgentResolver -> LocalSubagentExecutor -> PluginAgentExecutor -> A2AExecutor -> AgentGroupExecutor -> announce_result() -> MessageBus(system) -> AgentLoop -> user ``` 也就是说: 1. `spawn` 工具保留 2. `SubagentManager` 不再是唯一执行器 3. `DelegationManager` 成为真正总入口 4. skills 里的 `agent_cards` 用 frontmatter metadata 承载 5. workspace agent 单独持久化 6. group 通过并发 executor + 汇总消息实现 这是当前仓库里最稳妥、最符合现有架构的改法。 ## 15. 外部参考 以下是我写这个方案时核对的 A2A 资料: 1. A2A Protocol Development Guide: https://a2aprotocol.ai/docs/guide/a2a-typescript-guide.html 2. Python A2A Tutorial: https://a2aprotocol.ai/docs/guide/python-a2a-tutorial-20250513 注意: 1. 当前公开文档里既能看到 `tasks/send`,也能看到 `message/send` 2. agent card 路径也能看到 `agent-card` 与 `agent.json` 两种写法 3. 所以实现时建议做兼容层,不要只押一种命名