# 工具延迟加载覆盖（Defer / NoDefer）

CodeBuddy Code 支持在指定可用工具列表的同时，使用 `Defer(...)` / `NoDefer(...)` 修饰符**临时改变工具的延迟加载状态**，而无需改动全局配置。本文是该能力的完整参考。

> **背景**：默认情况下，工具是否走延迟加载由产品默认或 MCP 配置的 `defer_loading` 字段决定（详见 [MCP 文档 §延迟加载](mcp.md#延迟加载-defer_loading)）。这些是全局生效的默认设置。本特性允许你在不改默认配置的前提下，针对单次会话或单个自定义代理临时调整行为。

## 1. 语法速览

在任何接受工具名的「工具列表」字段里，普通工具名前加修饰符：

| 写法 | 含义 |
|---|---|
| `Read` | 普通工具，按全局默认决定是否 defer |
| `Defer(Glob)` | 该会话/代理强制让 `Glob` 走延迟加载（不直接进入模型的工具列表，可通过 `ToolSearch` 发现） |
| `NoDefer(Bash)` | 该会话/代理强制让 `Bash` 不走延迟加载（直接进入模型的工具列表） |
| `Defer(mcp__github__*)` | 通配匹配，把整组 MCP 工具置为延迟加载 |
| `Defer(*)` | 把当前列表内的所有工具都置为延迟加载（极端裁剪） |

**`*` 是唯一支持的通配字符**，匹配任意字符序列。其他字符（`?`、`[]` 等）按字面量处理。

## 2. 可用渠道

修饰符可以写在以下三类「工具列表」字段中：

### 2.1 CLI `--tools` 参数

```bash
codebuddy --tools "Read,Defer(Glob),NoDefer(Bash)"
```

ACP 与 SDK 客户端的 `tools` 字段与 CLI 等价。

### 2.2 自定义代理 frontmatter

在 `.codebuddy/agents/*.md` 的 YAML frontmatter 里：

```yaml
```

详见 [子代理 §配置字段](sub-agents.md#配置字段)。

## 3. 修饰符**不能**用在哪里

修饰符**仅作用于「工具列表」字段**。以下「权限规则」字段**不接受**修饰符：

* `--allowed-tools` / `settings.permissions.allow`
* `--disallowed-tools` / `settings.permissions.deny`
* hooks 配置中的 `matcher`

```bash
# ❌ 错误：会立即报错
codebuddy --allowed-tools "Defer(Glob)"
# Error: Defer(...) / NoDefer(...) modifiers belong in --tools, not in permission rule fields.

# ✅ 正确：分开写
codebuddy --tools "...,Defer(Glob)" --allowed-tools "Glob(src/**)"
```

**理由**：「暴露策略」（是否走延迟加载）与「行为约束」（参数过滤）是两件正交的事，分开表达更清晰。

## 4. 优先级与合并

最终决策按以下优先级（从高到低）：

| 优先级 | 来源 | 说明 |
|:-:|---|---|
| 0a | 任一层 `NoDefer(X)` 命中 | **强制非 defer**（无论其他来源说什么） |
| 0b | 任一层 `Defer(X)` 命中 | 强制 defer |
| 1 | MCP 工具级 `tools[name].defer_loading` | MCP 静态配置 |
| 2 | MCP 服务器级 `defer_loading` | MCP 静态配置 |
| 3 | 环境变量 `CODEBUDDY_DEFER_TOOL_LOADING` | 全局开关 |
| 4 | 用户设置 `settings.deferToolLoading` | 全局开关 |
| 5 | 内置默认（CodeBuddy Code 出厂配置） | 兜底 |

### 关键规则

* **`NoDefer` 永远胜过 `Defer`**：即使自定义代理写了 `Defer(X)`，CLI `--tools` 写 `NoDefer(X)` 后，本次会话内 X 仍然不延迟加载——用户运行时表态优先。
* **CLI 与代理配置平等参与**：两个来源的修饰符按并集生效，不分先后；最终结果由「`NoDefer` 优先」这一条规则裁决。
* **同名重复**：同一字段中多次出现同一工具时，**最后一次出现的修饰符**胜出（与 `--allowedTools` 重复时的语义一致）。

## 5. 自动附加：`ToolSearch` 与 `DeferExecuteTool`

只要工具列表里出现了**至少一条** `Defer(...)`，CodeBuddy Code 会**自动**把以下两个工具加进去（如果还没在列表里）：

| 工具 | 作用 |
|---|---|
| `ToolSearch` | 让模型能搜索发现被延迟加载的工具 |
| `DeferExecuteTool` | 让模型实际调用被延迟加载的工具 |

也就是说，你可以这样写：

```bash
# 等价于 codebuddy --tools "Bash,Read,Defer(Edit),ToolSearch,DeferExecuteTool"
codebuddy --tools "Bash,Read,Defer(Edit)"
```

### 为什么自动附加

延迟加载的工具不直接进入模型的工具列表，模型必须通过 `ToolSearch` 发现并通过 `DeferExecuteTool` 调用。少了任意一个，`Defer(...)` 就成了"模型再也用不到这个工具"。自动附加让最常见的写法符合直觉。

### 通配护栏：`Defer(*)` 不会卷走自身

写 `Defer(*)` 时，理论上"所有工具"都会被延迟加载——包括 `ToolSearch` 和 `DeferExecuteTool` 自身，从而让 defer 工作流失效。为避免这种"自我封死"，自动附加会**同时**给这两个工具加上 `NoDefer` 护栏：

```bash
codebuddy --tools "Defer(*)"
# 行为等价于:
# codebuddy --tools "*,ToolSearch,DeferExecuteTool" + NoDefer(ToolSearch),NoDefer(DeferExecuteTool)
```

模型仍能看到并使用 `ToolSearch` / `DeferExecuteTool`，从而能搜索并调用其他被延迟加载的工具。

### 不会触发自动附加的情形

* 工具列表里**只有** `NoDefer(...)`、没有 `Defer(...)`：用户意图是把工具拉回直接调用，与 defer 工作流无关
* 工具列表里没有任何修饰符：保持原有行为
* 用户已显式列出 `ToolSearch` 或 `DeferExecuteTool`：幂等，不重复添加

## 6. 典型用例

### 6.1 临时把工具收进延迟加载，省 token

```bash
# 知道这次只用 Read/Edit，把其他大型工具暂时收起来
codebuddy --tools "Read,Edit,Defer(Bash),Defer(Glob),Defer(Grep)"
```

模型上下文里只看到 `Read`、`Edit`、`ToolSearch`，需要时再通过 `ToolSearch` 调出其他工具。

### 6.2 临时强制让被 defer 的工具直接可用

```bash
# 假设 Bash 默认被配为延迟加载，但本次需要它直接可调用
codebuddy --tools "default,NoDefer(Bash)"
```

> 注：`default` 表示「全部内置工具」，与修饰符可同时使用。

### 6.3 自定义代理的精细化暴露策略

```yaml
---
name: explorer
description: 大型仓库探索专用代理，把搜索类工具收起来减少干扰
tools:
  - Read
  - Edit
  - ToolSearch
  - Defer(Glob)
  - Defer(Grep)
  - Defer(LSP)
---
```

### 6.4 整组 MCP 工具按需暴露

```bash
codebuddy --tools "default,Defer(mcp__github__*)"
```

把 GitHub MCP 服务器的所有工具收进延迟加载，需要时模型通过 `ToolSearch` 搜索 `github pr`、`github issue` 等关键词找到。

## 7. 错误排查

### 7.1 写错语法

```text
Error: Invalid --tools value: Invalid tool spec "Defer(Read(*.md))": Defer(...) only accepts a tool name or glob; permission filters like Read(*.md) belong in --allowed-tools, not --tools.
```

→ 把参数过滤搬到 `--allowed-tools`：

```bash
codebuddy --tools "Defer(Read)" --allowed-tools "Read(*.md)"
```

### 7.2 修饰符写到了错误的字段

```text
Error: Invalid permission rule "Defer(Glob)" in --allowed-tools / settings.permissions.allow: Defer(...) / NoDefer(...) modifiers belong in --tools, not in permission rule fields.
```

→ 见 §3。

### 7.3 嵌套 / 空内容

* `Defer(NoDefer(X))`：嵌套修饰被拒，无意义语义
* `Defer()`：空内容被拒
* `defer(Read)`：小写不识别，按字面量「含括号的工具名」拒绝

### 7.4 在自定义代理里出现拼写错误

自定义代理 frontmatter 中的 `tools` 字段使用宽松校验：单条非法写法只会发出警告日志后被忽略，不会让 CodeBuddy Code 启动失败。从日志中查找形如下面的提示来定位：

```text
[AgentToolSpec] my-agent: invalid-pattern — Invalid tool spec "Defer(Read(*.md))": ...
```

CLI 与 ACP 客户端直接输入的 `--tools` 使用严格校验，遇到非法写法会立刻报错。

## 8. 与「权限规则」的协作

修饰符与 `--allowed-tools` 完全正交，可以同时使用：

```bash
codebuddy \
  --tools "Read,Write,Defer(Glob)" \
  --allowed-tools "Glob(src/**)"
```

含义：

* `Glob` 走延迟加载（不直接进入模型的工具列表）
* 当模型通过 `ToolSearch` → `DeferExecuteTool` 调用 `Glob` 时，仍受 `Glob(src/**)` 权限约束
* 调用 `Glob(/etc/*)` 会按权限规则被拒绝

## 9. 与 ToolSearch / DeferExecuteTool 的交互

修饰符直接影响这两个工具的行为：

| 修饰 | 对 `ToolSearch` 的影响 | 对 `DeferExecuteTool` 的影响 |
|---|---|---|
| `Defer(X)`（X 原本不延迟加载） | 首次访问时自动加入可被搜索的延迟工具集合 | 首次调用时自动注册并放行 |
| `NoDefer(X)`（X 原本延迟加载） | 不会出现在搜索结果中（X 已直接进入模型的工具列表，无需搜索） | 调用 X 时**返回错误**，引导直接调用 |

> 报错示例（NoDefer 路径）：
> ```text
> Error: Tool "Bash" is not deferred in this session (NoDefer modifier). Call it directly instead of via DeferExecuteTool.
> ```

## 10. 已知边界

* **MCP 服务器整体名**：暂不支持在 `--tools` 里直接以服务器名作为修饰目标，只能写工具名或通配（如 `Defer(mcp__github__*)`）
* **通配反向枚举**：`Defer(mcp__*)` 等通配条目无法反向枚举具体工具名，MCP 工具的懒索引仍依赖 mcp-server-manager 自身的异步驱动流程
