# 多 Agent 团队 (`team/`)
基于**文件邮箱 (file inbox)** 的轻量级多 Agent 协作系统,参考 Claude Code Agent Teams 设计。
## 架构总览
```mermaid
graph LR
subgraph Team["src/agent/team/"]
direction TB
Mod["mod.rs (11 lines)
模块声明与重导出"]
Config["config.rs (53 lines)
TeamConfig, MemberConfig, MemberStatus"]
Inbox["inbox.rs (110 lines)
文件邮箱 (append-only JSONL + drain)"]
Manager["manager.rs (205 lines)
spawn/stop/send/broadcast/check_inbox/list"]
Teammate["teammate.rs (270 lines)
队友 ReAct 循环
(idle poll → react turn → report result)"]
end
Tools["src/agent/tools/team.rs (292 lines)
4 个团队工具暴露给 Lead Agent"]
Manager --> Tools
Teammate --> Manager
Inbox --> Manager
Inbox --> Teammate
```
```mermaid
graph TD
subgraph Lead["Lead Agent (AgentRuntime)"]
LR["ReAct Loop"]
TR["ToolRegistry"]
end
subgraph TM["TeamManager"]
Config["TeamConfig
session_id + members[]"]
Handles["HashMap<name, TeamMemberHandle>"]
end
subgraph InboxFS[".team/{session_id}/inbox/"]
L["lead.jsonl"]
S["searcher.jsonl"]
R["reader.jsonl"]
end
subgraph Teammates["Teammates (tokio::spawn)"]
T1["searcher
ReAct loop
max 5 steps"]
T2["reader
ReAct loop
max 5 steps"]
end
LR -->|"spawn_teammate"| TM
LR -->|"send_teammate_message"| S
LR -->|"check_team_inbox"| L
LR -->|"team_broadcast"| S
LR -->|"team_broadcast"| R
TM -->|"tokio::spawn"| T1
TM -->|"tokio::spawn"| T2
T1 -->|"append result"| L
T2 -->|"append result"| L
T1 -->|"drain inbox"| S
T2 -->|"drain inbox"| R
```
## 通信机制:文件邮箱
消息传递不通过 channel 或共享内存,而是通过 **append-only JSONL 文件**:
```
.team/{session_id}/inbox/
├── lead.jsonl ← 队友将结果写入此处
├── searcher.jsonl ← Lead 将任务写入此处
└── reader.jsonl ← Lead 将任务写入此处
```
### 消息类型 (`TeamMessage`)
| 字段 | 类型 | 说明 |
|:---|:---|:---|
| `from` | `String` | 发送者名称 |
| `to` | `String` | 接收者名称 |
| `content` | `String` | 消息内容 |
| `msg_type` | `TeamMessageType` | `task` / `result` / `question` / `answer` / `status` |
| `timestamp` | `String` | ISO 8601 时间戳 (UTC) |
### 核心操作
- **`append_message(team_dir, agent_name, msg)`**:追加一行 JSON 到接收者文件,自动创建目录
- **`drain_inbox(team_dir, agent_name)`**:读取所有消息行 → 清空文件 → 返回 Vec(破坏性读取)
- **`has_pending(team_dir, agent_name)`**:检查文件是否存在且非空(用于快速轮询)
> **已知问题**:`drain_inbox()` 在 `read_to_string` 和 `write("")` 之间存在 TOCTOU 竞态条件。若清空前有新消息追入,该消息会丢失。生产环境应使用 advisory file lock 或原子 rename 替代。
## 队友生命周期
```
┌──────────┐
│ SPAWNING │ ← Manager 创建句柄,tokio::spawn 启动循环
└────┬─────┘
▼
┌──────────┐ 有新消息到达 ┌──────────┐
│ IDLE │ ─────────────────→ │ WORKING │
│ │ ←──────────────── │ │
│ 5s×12 │ 任务完成/超时 │ ReAct │
│ 轮询 │ │ max 5步 │
└────┬─────┘ └──────────┘
│ 收到取消信号
▼
┌──────────┐
│ SHUTDOWN │ ← 发送告别 Status 消息给 Lead,退出循环
└──────────┘
```
### IDLE 阶段细节 (`teammate.rs:68-92`)
1. 设置状态为 `Idle`
2. `drain_inbox()` 检查是否有待处理消息
3. 若为空:以 **5 秒间隔轮询 `has_pending()`**,最长 60 秒(12 次 × 5 秒)
4. 在轮询中,若 `cancelled` 标志被设置或 `has_pending()` 返回 true,则提前退出
5. 超时无消息 → 回到步骤 2
6. 有新消息 → 进入 WORKING
### WORKING 阶段细节 (`teammate.rs:103-152`)
1. 设置状态为 `Working`
2. 将收件箱消息作为 `user` 角色消息注入到队友的对话历史中
3. 调用 `run_teammate_react_turn()`:最多 **5 步**的简化 ReAct 循环
4. 若返回结果 → 封装为 `TeamMessageType::Result` 发送到 `lead` 的收件箱
5. 若返回 `None`(错误/超时)→ 静默回到 IDLE(**不通知 Lead 出错**)
## 队友 ReAct 循环 vs 主 Agent 循环
| 特性 | 主 Agent (Lead) | 队友 (Teammate) |
|:---|:---|:---|
| 最大步数 | 8(可配置) | 5(硬编码 `min(config.max_steps, 5)`) |
| 流式输出 | SSE 实时推送到前端 | 无 SSE,静默消费 |
| 工具注册表 | 完整(含 subagent、team 工具) | 排除了 subagent 和 team 工具 |
| 上下文压缩 | 四层压缩 + CircuitBreaker | 简单 token 估算 + `compact::compress_context` |
| Hooks | PreToolUse / PostToolUse / Stop | 无 |
| 权限检查 | PermissionChecker | 无 |
| 数据库持久化 | agent_messages 表 | 无 |
| 后台通知 | BgNotificationQueue | 有 Queue 但未使用 |
| Skills | 两层加载 (list + load_skill) | 无,skill_registry 仅用于工具构造 |
队友的工具注册表构造 (`teammate.rs:40-41`):
```rust
let tool_registry =
ToolRegistry::new_with_queue(Some(queue.clone()), app_state.skill_registry.clone());
```
`ToolRegistry::new_with_queue()` 不注册 subagent 和 team 工具,防止无限委托链。
## TeamManager 设计
### 锁顺序约定 (CRITICAL)
存在两个 `tokio::sync::Mutex` 的嵌套获取:
1. `TeamManager.handles`(外层)
2. `TeamMemberHandle.status`(内层)
所有代码必须遵守 **handles → status** 的顺序,否则死锁。`list_members()` 作为正确顺序的参考实现。
### 主要操作
| 操作 | 说明 | 锁行为 |
|:---|:---|:---|
| `spawn(name, role)` | tokio::spawn 队友循环,注册句柄 | 获取 handles 锁 |
| `stop(name)` | 设置 cancelled 标志 | 获取 handles 锁 |
| `stop_all()` | 停止所有队友 | 获取 handles 锁 |
| `send_message(from, to, content, type)` | 追加消息到接收者收件箱文件 | 无锁(纯文件 I/O) |
| `broadcast(from, content)` | 向 config.members 中所有非发送者成员追加消息 | 无锁(纯文件 I/O) |
| `check_inbox(agent_name)` | drain_inbox 并返回消息 | 无锁(纯文件 I/O) |
| `list_members()` | 获取所有队友的 (name, role, status) | handles → 各 status(正确顺序) |
### 队友系统提示词 (`manager.rs:70-93`)
队友收到的是天体物理学研究助手角色设定 + 工具使用指引 + 通信协议说明。系统提示词始终通过 `build_teammate_system_prompt(role)` 生成,**不使用** `MemberConfig.system_prompt` 字段。
## 4 个团队工具
工具定义在 `src/agent/tools/team.rs`,均设置 `InterruptBehavior::Block`(中断时先完成副作用再停止):
| 工具名 | 参数 | 功能 | 接收者 |
|:---|:---|:---|:---|
| `spawn_teammate` | `name`, `role` | 启动一个后台队友 | — |
| `send_teammate_message` | `to`, `content` | 向指定队友发送消息 | 队友收件箱 |
| `team_broadcast` | `content` | 向所有队友广播消息 | 所有队友收件箱 |
| `check_team_inbox` | `agent_name?` (默认 "lead") | 读取并清空收件箱 | Lead 收件箱 |
典型协作流程:
```
1. Lead 调用 spawn_teammate(name="searcher", role="ADS文献搜索专家")
2. Lead 调用 send_teammate_message(to="searcher", content="搜索2024年暗物质间接探测综述")
3. Lead 继续自己的 ReAct 循环(可以同时发起另一个 spawn_teammate)
4. 队友在后台执行 ReAct 循环(最多5步),完成后将结果写入 lead.jsonl
5. Lead 调用 check_team_inbox() 读取队友结果
6. Lead 综合队友结果,给出最终回答
```
## 与子代理 (`subagent`) 的对比
代码库中存在**两种委托机制**,适用不同场景:
| | 子代理 (`subagent`) | 团队 (`team`) |
|:---|:---|:---|
| 执行模式 | 同步:Lead 等待完成 | 异步:后台运行,Lead 主动轮询 |
| 并发度 | 每次调用 1 个 | 可同时运行多个队友 |
| 上下文 | 任务完成后丢弃 | 跨任务累积(直到压缩) |
| 通信方式 | 工具调用 → 返回摘要 | 文件邮箱:写入/轮询 |
| Hooks | PreToolUse / PostToolUse / SubagentStart / SubagentStop | 无 |
| 权限检查 | PermissionChecker | 无 |
| 进度透传 | SSE 事件到父代理 | 无 |
| DB 持久化 | agent_messages 表 | 无 |
| 适用场景 | "去完成这个子任务,给我摘要" | "留在后台,持续处理我分配的任务" |
## 当前状态:未接入
> **重要**:`TeamManager` 和 4 个团队工具已完整实现,但当前**未接入 AgentRuntime**。
>
> `AgentRuntime::new()` 调用 `ToolRegistry::new_with_queue()`,而非 `ToolRegistry::new_with_team()`。团队工具通过独立的 `add_team_tools()` 函数接入,但该函数在当前代码路径中从未被调用。
>
> 实际可用的委托功能由 `subagent` 工具提供(通过 `SubAgentTool` 已接入)。
>
> 接入方法:在 `AgentRuntime::new()` 和 `with_config()` 中,创建 `TeamManager` 实例并用 `ToolRegistry::new_with_team()` 替代 `new_with_queue()`。
## 改进建议
1. **修复 drain_inbox 竞态条件**:使用 advisory file lock 或原子 rename
2. **队友错误传播**:`run_teammate_react_turn()` 返回 `None` 时应通知 Lead
3. **消息关联 ID**:添加 `correlation_id` 字段,支持请求-响应匹配
4. **降低锁持有时间**:将 `send_message` / `broadcast` 的文件 I/O 移出锁临界区(当前已是无锁,但工具层仍持有 `team_manager` 锁)
5. **队友名称去重**:spawn 同名队友前检查是否已有活跃队友
6. **会话级清理**:Agent 会话结束时调用 `stop_all()` 清理队友和收件箱文件