第一次提交

This commit is contained in:
2026-03-13 16:40:08 +08:00
commit 0a49bcfb2d
277 changed files with 61890 additions and 0 deletions

View File

@ -0,0 +1,753 @@
# 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. 所以实现时建议做兼容层,不要只押一种命名