数据截至 (上游 commit 9fb509bab3e0)
工具系统:MCP 工具 + 本地 agent 工具
本章一句话: 模型眼里只有「一堆工具」,根本不知道某个工具是外部 MCP 服务器提供的、还是 DeepChat 自带的本地能力——这层屏蔽就是工具系统的价值。
1. 它要解决的小问题
DeepChat 的「手脚」来自三类异构来源:外部 MCP 服务器(标准协议)、本地 agent 工具(读写文件、跑命令、搜代码……直接在主进程做)、以及 ACP agent 自带工具。它们调用方式、权限模型都不同。如果让聊天 runtime 去分辨「这个工具该怎么调」,会到处是 if-else。ToolPresenter 把它们抹平成一个统一接口。
2. 思路:聚合 + 来源映射
怎么读:左边是聊天 runtime,它只跟 ToolPresenter 打交道。ToolPresenter 内部
用 ToolMapper 记住「每个工具名来自哪」,调用时按来源分流。
AgentRuntimePresenter
│ getAllToolDefinitions() ← 拿统一的 MCPToolDefinition[]
│ callTool(name, args) ← 按名字调,不关心来源
▼
┌─────────────── ToolPresenter ───────────────┐
│ ToolMapper: toolName ─► 'mcp' | 'agent' │
└───────┬───────────────────────────┬──────────┘
│ source = mcp │ source = agent
▼ ▼
┌───────────────┐ ┌────────────────────────┐
│ McpPresenter │ │ AgentToolManager │
│ 外部 MCP 服务 │ │ ├ FileSystemHandler │
└───────────────┘ │ ├ BashHandler (exec) │
│ ├ FffSearchHandler │
│ ├ chatSettingsTools │
│ ├ SubagentOrchestrator │
│ ├ AgentPlanTool │
│ └ TapeTools / MemoryTools│
└────────────────────────┘
3. 聚合工具定义
ToolPresenter.getAllToolDefinitions() 按顺序做(见 docs/architecture/tool-system.md):
- 从
mcpPresenter拉 MCP 工具。 - 从
AgentToolManager拉本地 agent 工具。 - 用
ToolMapper记来源;重名时优先保留 MCP 工具。 - 过滤 disabled 的 agent 工具,并为每个 conversation 维护独立映射。
结果:agentRuntimePresenter 只持有统一的 MCPToolDefinition[],完全不知道某个工具真实出身。这就是上一章 processStream 里 tools 参数的来源。
4. 本地 agent 工具有哪些
AgentToolManager 装配的 model-facing 工具(名字见 agentToolManager.ts):
| 工具名 | 干什么 | 处理器 |
|---|---|---|
read / write / edit | 读写/编辑文件 | AgentFileSystemHandler |
glob / grep | 代码/文件搜索(FFF backed) | AgentFffSearchHandler |
exec / process | 执行命令 / 管理后台进程 session | AgentBashHandler |
question(QUESTION_TOOL_NAME) | 反问用户一个结构化问题 | runtime 内置 |
update_plan(agent-core/update_plan) | 更新任务计划(只改 plan state) | AgentPlanTool |
subagent_orchestrator | 编排子 agent 并行/串行跑任务 | SubagentOrchestratorTool |
image_generate | 图像生成 | AgentImageGenerationTool |
tape_info/tape_search/tape_anchors/tape_handoff/tape_context | 读/搜索磁带、打 handoff anchor | AgentTapeToolHandler |
skill_list / skill_view | 列出/查看 Skills | SkillTools |
搜索策略很明确:prompt 引导模型走 glob → grep → read,shell 搜索命令不在 model-facing 代码搜索路径上(docs/architecture/tool-system.md,FFF Search 一节)。
5. 权限:预检查 + 暂停授权
本地工具不直接碰旧 presenter,而是通过注入的 runtime port(toolPresenter/runtimePorts.ts,AgentToolRuntimePort)拿到:conversation 工作目录、已批准路径、settings 审批消费、会话上下文桥接。
权限按能力拆成三个服务:
| 能力 | 权限服务 | 触发的错误 |
|---|---|---|
| 文件访问 | filePermissionService | FilePermissionRequiredError |
| settings 变更 | settingsPermissionService | — |
| shell/命令 | CommandPermissionService | CommandPermissionRequiredError |
流程上,危险操作不是「先做了再问」:AgentToolManager 在调用前做 preCheckToolPermission,需要授权就抛出对应的 *PermissionRequiredError;processStream 把它转成 pending interaction,循环暂停,授权块回到 renderer 等用户点批准(见 02-chat-loop.md 的「三个出口」)。这也是为什么 read 这种只读工具被列进 PARALLEL_READ_ONLY_AGENT_TOOLS(dispatch.ts:159)——只读可并行,写操作要串行 + 授权。
6. Subagent:把活拆给子会话,再 merge / discard 结果
子 agent 编排在 HEAD 换了形态:旧的一次性 subagent_orchestrator 工具(一次派最多 5 个任务)被改成了操作式的 live delegation 工具 deepchat_subagents(LIVE_DELEGATION_AGENT_TOOL_NAME,src/shared/agentTools.ts:3)——父 agent 用 spawn / send / follow_up / list / inspect / read_result / wait / interrupt 八种操作(LIVE_DELEGATION_OPERATIONS,src/shared/orchestration/liveDelegation.ts:31)对子 agent 做全生命周期管理,而不是一把梭交给编排器。schema 在 liveDelegationSchema(src/main/tool/agentTools/liveDelegationTool.ts:24),按 operation 校验必填字段。每个子任务仍跑在独立子会话里(sessionKind='subagent',见 docs/FLOWS.md 第 3 节)。
关键在结果如何回到父会话——这里和 Tape 紧密咬合:
src/main/tape/application/sessionTape.ts:489-495
mergeFork(parentSessionId, forkId) // 把子磁带吸收进父会话
discardFork(parentSessionId, forkId) // 整段丢弃(只追加标记)
子会话以 ${parentSessionId}::fork::${forkId} 的 fork 形式存在(01-tape-system.md 第 6 节,forkService.ts:90);跑完后父会话要么 mergeFork(把子磁带吸收进来)、要么 discardFork(整段丢弃)。因为一切都是 append-only,merge/discard 也只是「在父磁带上追加结果或一个丢弃标记」,不破坏任何过程。
7. 巧妙之处
- 来源透明:聊天 runtime 持有统一
MCPToolDefinition[],加新工具种类(本地/ MCP / ACP)不动主循环。 - 权限即暂停:把「要授权」实现成抛错 → 暂停交互,复用了 stream 循环已有的 paused 机制,不需要工具系统自己搞一套阻塞 UI。
- subagent = fork + live delegation:子 agent 不是临时内存对象,而是真正的 fork 会话;工具面从「一次编排」改成 spawn/wait/read_result 的操作循环,结果经
mergeFork/discardFork落到父会话,可追溯可丢弃。
8. 边界与局限
- 重名工具一律 MCP 优先,本地同名工 具会被遮蔽——加本地工具要避开常见 MCP 名。
- FFF 不可用时
glob/grep直接报工具错误(不静默降级),保证模型能感知搜索失败(docs/architecture/tool-system.md)。
9. 代码地图
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 聚合 / 路由 | src/main/tool/index.ts | ToolPresenter、getAllToolDefinitions |
| 来源映射 | src/main/tool/toolMapper.ts | ToolMapper |
| 本地工具装配 | src/main/tool/agentTools/agentToolManager.ts | AgentToolManager |
| runtime port | src/main/tool/runtimePorts.ts | AgentToolRuntimePort |
| 子 agent live delegation | src/main/tool/agentTools/liveDelegationTool.ts | liveDelegationSchema、LIVE_DELEGATION_AGENT_TOOL_NAME(deepchat_subagents) |
| Tape 工具 | src/main/tool/agentTools/agentTapeTools.ts | AgentTapeToolHandler、TAPE_TOOL_NAMES |
| 命令权限 | src/main/tool/permission/commandPermissionService.ts | CommandPermissionService、CommandPermissionRequiredError |