# Agent Note: 配置 subagent 的人设、工具可见性与深度

Status: implemented

[English](2026-07-12-subagent-persona-tool-filter-and-depth.md) | 中文

## 问题

一个可复用的 subagent 提供方解决的是「如何运行子 agent（智能体）」的问题，但不同的委派工具需要不同的子 agent 行为。某个部署可能需要评审者人设、仅限研究的工具集，或硬性递归上限，而不必为每种组合创建新的提供方。

这些控制影响子 agent 的第一次模型请求，因此不能在子 agent 可见之后再安装。它们还需要提供方的诚实支持：ACP（Agent Client Protocol）后端不能默默接受一个仅限进程内的工具过滤器，而过滤器在所有插件运行于同一可信进程的情况下也不应被描述为安全边界。

## 决策

subagent 启动有三个独立的组合控制：`persona`、`toolFilter` 和 `maxDepth`。提供方声明对每个控制的支持情况，服务在启动运行之前拒绝不受支持的请求，进程内提供方在子 agent 尚未发布时安装所请求的组合。

这些控制回答不同的问题：

| 控制 | 问题 | 结果 |
|---|---|---|
| `persona` | 什么角色指令替换该子 agent 的部署人设？ | 一个子 agent 局部的提示词段落遮蔽 `deployment:persona` |
| `toolFilter` | 部署全局工具中哪些进入该子 agent 的可见工具视图？ | 一个有作用域的限制在添加子 agent 局部工具之前过滤全局工具 |
| `maxDepth` | 这棵委派树最深可以长到多少层？ | 子 agent 深度超过绝对上限时，启动请求被拒绝 |

`dsh-tool-subagent` 将这些控制作为插件配置暴露，并复制到它创建的每个请求中。直接调用 `SubagentService` 的调用方可以按请求选择这些控制。提供方的能力描述符仍然是后端能否兑现各字段的真源。

### 人设是有作用域的遮蔽

人设控制改变一个子 agent 的行为，而不改变部署级的提示词组装。在未发布的设置阶段，进程内提供方在子 agent 作用域中注册一个名为 `deployment:persona` 的段落；普通的最具体者优先解析规则仅在该子 agent 的组装中替换全局段落。

其值与部署人设具有相同的严格模板语义。省略时通过全局层继承部署段落；显式空字符串则以空段落遮蔽全局人设。父级和兄弟级的人设永远不会进入子 agent 的扁平作用域。

这使用的是常规的系统提示词注册机制，而非第二条人设通道。因此第一次提示词看到的命名贡献与后续提示词和提示词检查工具看到的一致。

### 工具过滤是一条作用于全局视图的活规则

工具过滤同时控制能力可见性和可执行查找。进程内提供方在发布前于子 agent 作用域中安装 `ToolRegistry.restrict()`，注册表的单一解析器对协议格式（wire format）的工具 schema、查找、执行和 Code Mode SDK 生成施加相同的结果。独立注册的系统提示词段落不在 `ToolRegistry` 内，因此过滤一个工具不会移除该插件的独立指导文本。

解析遵循以下规则：

1. 每条限制对活跃的部署全局工具注册表先应用 `allow` 再应用 `deny`。
2. 多条限制取交集，因此每条已安装的限制都必须放行一个全局工具。
3. 子 agent 作用域的工具在全局过滤之后添加，可以遮蔽一个已放行的全局工具。
4. 保留的 `run_code` 呈现和其他作用域局部的协议贡献不受全局过滤器影响。

当过滤器既未提供 `allow` 也未提供 `deny`，或命名了当前全局可限制集合之外的内容（包括仅作用域局部或保留名称）时，配置会显式失败。`allow: []` 合法，且有意隐藏所有全局工具。这些检查能捕获拼写错误，并防止配置在无法影响所命名条目时看起来仍然有效。

全局注册表保持活跃。仅 deny 的过滤器会放行后来注册的全局名称（除非显式 deny 该名称）；allow 列表会排除后来注册的全局名称（除非显式 allow 该名称）。移除一个全局工具会将其从所有已解析视图中移除。这些语义在保持热注册的同时，使 allow 与 deny 的区别显式化。

### 深度是绝对的树上限

深度限制独立于工具可见性来约束递归委派。顶层 agent 深度为零；进程内子 agent 的深度为其父级已验证深度加一。`maxDepth` 是一个绝对的非负安全整数，当推导出的子 agent 深度大于上限时，启动在子 agent 所有权开始之前即被拒绝。

有效父级深度取持久 `SessionHeader.delegationDepth` 与运行时 `AgentOptions.subagentDepth` 中的较大值。进程内子 agent 把推导出的深度记录在会话 header 中，恢复时会重新载入该 header，因此重启无法降低递归计数。

每个公开入口都自行验证值域，而非依赖单一的面向模型配置路径。负值、小数、负零、非有限值、不安全整数、格式错误的存储父级深度以及推导溢出均被拒绝。直接的 `SubagentStartRequest` 可以省略上限，让此机制不约束深度；经 Loader 解析的 `dsh-tool-subagent` 配置则默认值为 `3`、接受数值覆盖，并使用显式的 `'provider-managed'` 来省略由进程外提供方部署拥有递归预算时的上限。三是一个较小的有限默认值，仍允许 root 加三代后代：[SDK 辅助函数生成的 subagent 条目](../../../../packages/sdk/helper/src/features/builtin/index.ts)和 [JSON-RPC 示例](../../../../examples/jsonrpc-agent/cordis.yml)采用这项通用策略，而已交付的交互式 ACP、headless 和 REPL 示例固定为一。提供方缺少 `depthLimit` 时，数值工具上限会在提供方挂载阶段失败。

部署可以组合深度与过滤，但数值上限不会合成过滤器。委派工具在上限处仍然可见，因为授权可能依赖运行时状态；每次尝试启动都会检查调用方 agent 当前的持久与运行时深度，被拒绝的启动返回错误工具结果，且不发布子 agent。可见性策略固定的部署可以另外在子 agent 中 deny 委派工具。两种选择都不改变提供方的对话历史行为。

### 能力门控保持提供方诚实

能力将请求的功能与提供方实现分离。`SubagentCapabilities` 声明 `persona`、`toolFilter` 和 `depthLimit`；`SubagentService.start()` 在调用提供方之前，对照这些标志检查请求中每个存在的字段。

这使 spawn 和 fork 提供方可以共享进程内实现，而外部提供方只声明自己能强制执行的部分。请求永远不会静默降级：选择不受支持的控制会产生 `UNSUPPORTED_CAPABILITY`，不会有运行或生命周期事件存在。

### 未发布设置使第一次请求正确

所有子 agent 局部的组合在子 agent 变得可观察之前完成。进程内提供方向 agent 创建提供一个设置回调；该回调在子 agent 作用域中安装人设、工具限制和结构化输出贡献。只有设置成功后，创建才发布会话和 agent 并允许驱动器启动。

设置失败会回滚私有子 agent。没有观察者能获取到一个「第一次提示词使用了部署人设或未过滤工具集、后续提示词才使用所请求配置」的子 agent。

## 可见性不是授权

这些控制组合的是同一可信进程内的行为，而非授权行为。`toolFilter` 改变工具注册表解析出的子 agent 视图，但它不创建父到子的授权格，不要求子 agent 仅持有父级子集授权，不沙箱化插件，也不阻止持有另一个 Cordis 上下文的代码直接调用服务。

具体而言，子 agent 局部工具在全局过滤之后添加，可能不在父级视图中。仅 deny 的子 agent 也能看到 deny 列表未命名的后来全局工具。这些是有意的活组合语义，而非不可升权保证。

安全设计需要独立的授权表示、传播规则和执行时强制点。创建时的授权快照、父级子集授权、显式的未来授权 API，以及通用的能力/输出/终止标签均不在本功能范围内。

## 曾考虑的替代方案

**为每种人设或工具集创建一个提供方。** 这会使共享相同传输和生命周期实现的提供方成倍增加，使动态部署配置变得笨拙，且仍需要递归机制。提供方的职责是执行传输；请求承载每个子 agent 的组合。

**复制父级的完整工具视图。** 注册作用域设计上是扁平的，生命周期所有权不意味着可见性继承。复制已解析视图还会冻结动态全局注册，并在未完整定义任一契约的情况下混淆组合与授权。

**在子 agent 创建时快照允许的全局工具。** 冻结的 allow 集合使未来注册统一不可用，但它改变了热注册语义并开启了授权设计。已实现的过滤器保持为活跃的注册表谓词，并直接记录 allow 与 deny 的行为。

**仅隐藏工具 schema。** 仅呈现层的过滤让模型可以通过 Code Mode 或伪造调用执行一个提示词声称不存在的工具。改为由一个解析器同时管控呈现和执行。

**把深度上限编码为自动工具过滤器。** 创建时过滤器会快照一个可能依赖运行时状态的决策，只影响一个已配置工具名，且不保护直接服务调用方或替代委派工具。提供方改为在每次启动时强制绝对上限。

## 后果

贡献者可以配置子 agent 的角色、可见全局工具和递归深度，而无需定义新的提供方。能力检查在所有权开始之前失败，未发布设置使第一次请求一致，单一工具解析器防止呈现/执行漂移。

代价是部署方必须理解活跃的 allow/deny 行为以及可见性与授权的区别。当前深度策略禁止再创建子 agent 后，模型仍可能调用可见的委派工具并收到错误。提供方作者必须准确声明每个受支持的控制，进程内提供方必须在发布前安装所有请求的贡献。这些控制有意不解决安全隔离或父到子的不可升权问题。
