Docker 容器化部署
- 提供 Mode A (Alpine musl, ~23MB) 和 Mode B (Distroless glibc, ~87MB)
两种镜像,Docker Compose 一键启动
- build.rs 支持 SKIP_DASHBOARD_BUILD 跳过前端构建
- 国内镜像加速 (npm/apt/apk) 通过 USE_MIRRORS build-arg 控制
安全:Cookie-Based 鉴权系统
- HttpOnly/SameSite=Strict Cookie 会话管理(24h 过期自动清理)
- 登录/登出/验证接口 + 中间件注入
- 前端登录页面 + 退出按钮
- 三层 CORS:localhost 鉴权 / 全放通 bookmarklet / 受保护路由
- 书签脚本 fetch 添加 credentials:'include'
Coordinator 模式 (P2)
- 4 个 meta-tool (delegate_task/check_task/task_stop/synthesize)
- WorkerPool + Semaphore 并发控制 + 超时保护
- 前端协调者模式开关
Hook 系统:UserPromptSubmit 事件 (P2)
- 第 13 个生命周期事件,fire-and-forget 审计
FTS5 全文搜索 (P3)
- agent_sessions_fts + agent_messages_fts 虚拟表
- search_history Agent 工具 + /api/search/history HTTP 接口
- 前端防抖搜索框 + 仅当前会话筛选
工具加载优化 (P3)
- defer_loading 延迟加载 (7 个重型工具)
- is_readonly 只读标记 (9 个查询工具)
- classifier_summary 工具目录供 LLM 按需判断
模型回退策略 (P3)
- LLM_FALLBACK_MODEL 优先回退 + LLM_FALLBACK_CHAIN 链式轮换
- LlmClient model 改为 Arc<RwLock> 支持运行时切换
- 连续 3 次过载后自动切换
压缩记忆桥接 (P3)
- 压缩丢弃消息 → 子代理提取持久记忆 (extract_memories_from_compaction)
git2 依赖修复
- 切换到 vendored-libgit2,消除 OpenSSL 系统依赖
10 KiB
多 Agent 团队 (team/)
基于文件邮箱 (file inbox) 的轻量级多 Agent 协作系统,参考 Claude Code Agent Teams 设计。
架构总览
graph LR
subgraph Team["src/agent/team/"]
direction TB
Mod["mod.rs (11 lines)<br/>模块声明与重导出"]
Config["config.rs (53 lines)<br/>TeamConfig, MemberConfig, MemberStatus"]
Inbox["inbox.rs (110 lines)<br/>文件邮箱 (append-only JSONL + drain)"]
Manager["manager.rs (205 lines)<br/>spawn/stop/send/broadcast/check_inbox/list"]
Teammate["teammate.rs (270 lines)<br/>队友 ReAct 循环<br/>(idle poll → react turn → report result)"]
end
Tools["src/agent/tools/team.rs (292 lines)<br/>4 个团队工具暴露给 Lead Agent"]
Manager --> Tools
Teammate --> Manager
Inbox --> Manager
Inbox --> Teammate
graph TD
subgraph Lead["Lead Agent (AgentRuntime)"]
LR["ReAct Loop"]
TR["ToolRegistry"]
end
subgraph TM["TeamManager"]
Config["TeamConfig<br/>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<br/>ReAct loop<br/>max 5 steps"]
T2["reader<br/>ReAct loop<br/>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)
- 设置状态为
Idle drain_inbox()检查是否有待处理消息- 若为空:以 5 秒间隔轮询
has_pending(),最长 60 秒(12 次 × 5 秒) - 在轮询中,若
cancelled标志被设置或has_pending()返回 true,则提前退出 - 超时无消息 → 回到步骤 2
- 有新消息 → 进入 WORKING
WORKING 阶段细节 (teammate.rs:103-152)
- 设置状态为
Working - 将收件箱消息作为
user角色消息注入到队友的对话历史中 - 调用
run_teammate_react_turn():最多 5 步的简化 ReAct 循环 - 若返回结果 → 封装为
TeamMessageType::Result发送到lead的收件箱 - 若返回
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):
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 的嵌套获取:
TeamManager.handles(外层)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(中断时先完成副作用再停止)。P3 工具分类注解已应用:
| 工具名 | 参数 | 功能 | 接收者 | 延迟加载 | 只读 |
|---|---|---|---|---|---|
spawn_teammate |
name, role |
启动一个后台队友 | — | ✅ defer_loading |
— |
send_teammate_message |
to, content |
向指定队友发送消息 | 队友收件箱 | ✅ defer_loading |
— |
team_broadcast |
content |
向所有队友广播消息 | 所有队友收件箱 | ✅ defer_loading |
— |
check_team_inbox |
agent_name? (默认 "lead") |
读取并清空收件箱 | Lead 收件箱 | — | ✅ is_readonly |
延迟加载说明(P3):
spawn_teammate、send_teammate_message、team_broadcast标记为defer_loading=true,不随常驻工具注入 system prompt,仅当 LLM 判断需要多 Agent 协作时通过tool_catalog按需加载。check_team_inbox标记为is_readonly=true(只读取文件邮箱,不修改队友状态),在 auto 模式下可跳过权限确认。
典型协作流程:
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()。
改进建议
- 修复 drain_inbox 竞态条件:使用 advisory file lock 或原子 rename
- 队友错误传播:
run_teammate_react_turn()返回None时应通知 Lead - 消息关联 ID:添加
correlation_id字段,支持请求-响应匹配 - 降低锁持有时间:将
send_message/broadcast的文件 I/O 移出锁临界区(当前已是无锁,但工具层仍持有team_manager锁) - 队友名称去重:spawn 同名队友前检查是否已有活跃队友
- 会话级清理:Agent 会话结束时调用
stop_all()清理队友和收件箱文件