# 工具协议 · 进程内与跨进程一套契约

> grok 的工具默认在进程内跑，但同一套 ToolHandle/流式契约能无缝跨进程。这一章拆三层协议栈（types/runtime/protocol）、一个 object-safe 的 ToolHandle 如何同时套住本地工具与远程代理，以及那套 JSON-RPC「Computer Hub」线协议。

- 作者：David（道雾轩）
- 专栏：Grok Build 源码解读（https://daiw.org/manual/grok-build.md）
- 最后更新：2026-09-15
- 原文：https://daiw.org/manual/grok-build/tool-protocol
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# 工具协议 · 进程内与跨进程一套契约

上一章的派发止于 `local_registry.find`。这一章往下挖一层：那个被 find 出来的 **`ToolHandle`** 到底是什么——以及为什么它能让「同一份工具契约，在同进程或跨进程都成立」。

## 三层协议栈

工具契约摊在三个通用 crate 上，层层递进：

| crate | 层 | 管什么 |
| --- | --- | --- |
| `xai-tool-types` | 叶子 | 纯共享类型(描述、任务工具的 wire 类型)，所有上层都依赖它，保证形状不漂移 |
| `xai-tool-runtime` | 进程内契约 | `Tool` / `ToolDyn` / `ToolDispatch` / `ToolStream`，流式执行 |
| `xai-tool-protocol` | 线协议 | JSON-RPC 2.0「Computer Hub」，跨进程的方法与帧 |

## 一个 object-safe 的 `ToolHandle` 套住两种实现

关键抽象是 `ToolHandle`(`xai-computer-hub-core/src/resolver.rs`,object-safe、`#[async_trait]`):`id()`、`description()`、`capabilities()`、`execute(ctx, args) -> ToolStream`。它有**两种实现**，外面完全一视同仁：

- **进程内**：`ErasedTool<T: Tool>`——解 JSON 参数 → 驱动 `Tool::execute` → 把 `T::Output` 编回 `TypedToolOutput`。装在 `LocalRegistry`(`xai-computer-hub-sdk` 里一个保序的 `IndexMap<ToolId, Arc<dyn ToolHandle>>`)里。
- **跨进程**：`RemoteToolProxy`——它的 `execute` 把调用变成一个 `tool_call_request` 发过连接，按 `tool_call_id` 订阅进度，再把进度帧 + 终止帧合并回**同一种 `ToolStream`**。

```mermaid
flowchart TB
  D["派发:resolve_and_dispatch"] --> RH{ToolHandle}
  RH -->|同进程| ET["ErasedTool T<br/>直接驱动 Tool"]
  RH -->|跨进程| RP["RemoteToolProxy<br/>JSON-RPC 发到 tool server"]
  ET --> TS["ToolStream: Progress* + Terminal"]
  RP --> TS
```

grok-build **只用进程内那套**(`FinalizedToolset.local_registry`)，但因为工具藏在 `ToolHandle` 后面，那套「进度流」契约无论工具在同进程还是隔进程，**代码长得一模一样**。这就是「一套契约、两种传输」的威力。

## 分裂的 trait：类型化的窄门 + 擦除的宽门

这里有个精妙的 Rust 设计，值得单独点出：

- **`Tool`** 用 **RPITIT**(`async fn` in trait，带显式 `Send`)——不装箱、不 object-safe、只能泛型使用。窄，但零开销、类型全在。
- **`ToolDyn`** 用 `#[async_trait]`——装箱、object-safe。宽，能塞进 `Vec<Arc<dyn ...>>`。

一个 blanket `impl<T: Tool> ToolDyn for T` 把前者自动升级成后者。**类型擦除只在注册那一刻发生一次**，dyn 兼容的分界线画得清清楚楚。这是「既要静态派发的性能、又要动态集合的灵活」时的标准答案。

## 线协议：三方模型

`xai-tool-protocol` 定义了一套 JSON-RPC 2.0 协议(方法是封闭的 `Method` 枚举)，是**三方**的：**harness ↔ hub(服务)↔ tool_server**。`TransportKind { Local, Remote }` 就是那个「进程内 / 跨进程」开关。能力协商在 `ToolCapabilities`(streaming、supports_cancel、max_concurrency、`ToolScope { Read, Write }`……)。

一个亮点是 `ToolDefinitionMode`:

- **`Full`**：把所有工具定义摊给模型。
- **`Concise { meta_search, meta_call }`**：协议为「工具太多、不能全塞给模型」预留的模式——只暴露一个搜索元工具和一个调用元工具，让模型先搜再调。不过 grok CLI 并没有构造这个枚举(全仓只用过 `Full`)，而是在本地常开一套等价机制：每轮都把 MCP 工具从给模型的工具列表里滤掉，只留内置的 `search_tool` / `use_tool` 两个**元工具**(见 [MCP 注册表](https://daiw.org/manual/grok-build/mcp-registry))。这正是[主循环](https://daiw.org/manual/grok-build/agent-loop)里那个 `use_tool` 的来历。

此后这条线协议还长出一块 **bot-relay** 面(`bot_relay.rs`)：`bot.command`、`bot.roster`、`bot.subscribe`、`bot.event` 等方法，连 Kotlin/Swift 客户端类型都是从它生成的；`xai-computer-hub-core/src/bot_tools.rs` 另登记了一批由 hub 合成的「Grok Bot」工具(`bot_create_agent`、`bot_send_prompt`……)。同一条管道，开始承载「远程指挥 agent」这件事。

<Callout type="info">
  还有个多 agent 协作的伏笔：`ToolScope { Read, Write }`——写操作只路由到 leader agent，避免多个并行 agent 同时改文件打架。这条线在[并行 worktree](https://daiw.org/manual/grok-build/fast-worktree)会再遇到。
</Callout>

## 和《从 LLM 到 Coding Agent》对照

那本的工具是进程内函数，不涉及跨进程。grok 的增量是——真实产品要接 MCP(可能是别的进程/机器上的工具服务器)，于是把工具契约抽象到「同进程还是跨进程都成立」的高度：一个 object-safe 的 `ToolHandle` + 一套流式协议 + 一层 JSON-RPC。这是「玩具能内联，产品要抽象传输层」的又一个范例。

至此工具系统的**框架**清楚了。下一章起进**具体工具**：读、搜、三种编辑格式、shell、沙箱……

---

上一篇：[工具注册与分发](https://daiw.org/manual/grok-build/tool-registry) · 下一篇：[读与搜](https://daiw.org/manual/grok-build/read-and-search)
