DSH / Atlas
2026-07-12implementedfeature

Configure subagent persona, tool visibility, and depth

配置 subagent 的人设、工具可见性与深度

A reusable subagent provider answers how to run a child, but different delegation tools need different child behavior. One deployment may want a reviewer persona, a research-only tool set, or a hard recursion bound without creating a new provider for every combination. These controls affect the child's first model request and therefore cannot be installed after the child is visible. They also need honest provider sup

English

Problem

A reusable subagent provider answers how to run a child, but different delegation tools need different child behavior. One deployment may want a reviewer persona, a research-only tool set, or a hard recursion bound without creating a new provider for every combination.

These controls affect the child's first model request and therefore cannot be installed after the child is visible. They also need honest provider support: an ACP backend cannot silently accept an in-process-only tool filter, and a filter must not be described as a security boundary when every plugin runs in the same trusted process.

Decision

Subagent starts have three independent composition controls: persona, toolFilter, and maxDepth. A provider advertises support for each control, the service rejects unsupported requests before starting a run, and an in-process provider installs the requested composition while the child is still unpublished.

The controls answer different questions:

ControlQuestionResult
personaWhat role instructions replace the deployment persona for this child?A child-local prompt section shadows deployment:persona
toolFilterWhich deployment-global tools enter this child's visible tool view?A scoped restriction filters globals before child-local tools are added
maxDepthHow deep may this delegation tree grow?A start whose child depth exceeds the absolute cap is rejected

dsh-tool-subagent exposes the controls as plugin configuration and copies them into each request it creates. Direct SubagentRuntime callers may choose them per request. The provider capability descriptor remains the source of truth for whether a backend can honor each field.

Persona is a scoped shadow

The persona control changes one child without changing deployment-wide prompt assembly. During unpublished setup, an in-process provider registers a child-scoped section named deployment:persona; ordinary most-specific-wins resolution replaces the global section only in that child's assemblies.

The value has the same strict template semantics as the deployment persona. Omitting it inherits the deployment section through the global layer; an explicit empty string shadows the global persona with an empty section. Parent and sibling personas never enter the child's flat scope.

This uses the normal system-prompt registration mechanism rather than a second persona channel. The first prompt therefore sees the same named contribution that later prompts and prompt-inspection tools see.

Tool filtering is one live global-view rule

The tool filter controls capability visibility and executable lookup together. An in-process provider installs ToolRuntime.restrict() in the child's scope before publication, and the registry's single resolver applies the same result to wire tool schemas, lookup, execution, and Code Mode SDK generation. Independently registered system-prompt sections are outside ToolRuntime, so filtering a tool does not remove that plugin's standalone guidance.

Resolution follows these rules:

  1. Each restriction applies allow before deny to the live deployment-global tool registry.
  2. Multiple restrictions intersect, so every installed restriction must admit a global tool.
  3. Child-scoped tools are added after global filtering and may shadow an admitted global tool.
  4. Reserved run_code presentation and other scope-local protocol contributions are outside the global filter.

Configuration fails loudly when a filter supplies neither allow nor deny, or names something outside the current global restrictable set, including a scope-local-only or reserved name. allow: [] is valid and deliberately hides every global tool. These checks catch misspellings and prevent configuration from appearing effective when it cannot affect the named entry.

The global registry remains live. A deny-only filter admits a later global name unless it explicitly denies that name; an allow-list excludes a later global name unless it explicitly allows that name. Removing a global tool removes it from every resolved view. These semantics preserve hot registration while making the difference between allow and deny explicit.

Depth is an absolute tree cap

The depth limit bounds recursive delegation independently of tool visibility. A top-level agent has depth zero; an in-process child has its parent's validated depth plus one. maxDepth is an absolute non-negative safe integer, and a start rejects before child ownership begins when the derived child depth is greater than the cap.

The effective parent depth is the greater of durable SessionHeader.delegationDepth and runtime AgentOptions.subagentDepth. An in-process child records its derived depth in the session header, and resume restores that header, so a restart cannot lower the recursion count.

Every public entry validates the domain rather than relying on one model-facing configuration path. Negative values, fractions, negative zero, non-finite values, unsafe integers, malformed stored parent depth, and derived overflow all reject. A direct SubagentStartRequest may omit the cap to leave depth unbounded; loader-resolved dsh-tool-subagent configuration instead defaults to 3, accepts a numeric override, and uses explicit 'provider-managed' to omit the cap for an out-of-process provider whose deployment owns its recursion budget. Three is a small finite default that still permits a root plus three descendant generations: the JSON-RPC example uses that general policy, while the ACP and headless examples pin one. A numeric tool cap fails at provider mount when the provider lacks depthLimit.

A deployment can combine depth and filtering, but the numeric cap does not synthesize a filter. The delegation tool stays visible at the cap because authorization may depend on runtime state; every attempted start checks the calling agent's current durable and runtime depth, and a rejected start returns an errored tool result without publishing a child. A deployment may separately deny delegation tools in children when its visibility policy is static. Neither choice changes the provider's conversation-history behavior.

Capability gating keeps providers honest

Capabilities separate a requested feature from a provider implementation. SubagentCapabilities advertises persona, toolFilter, and depthLimit; SubagentRuntime.start() checks every present request field against those flags before calling the provider.

This lets spawn and fork providers share the in-process implementation while external providers advertise only what they can enforce. A request never degrades silently: selecting an unsupported control produces UNSUPPORTED_CAPABILITY, and no run or lifecycle event exists.

Unpublished setup makes the first request correct

All child-local composition is complete before the child becomes observable. The in-process provider supplies one setup callback to agent creation; that callback installs persona, tool restriction, and structured-output contributions in the child's scope. Only after setup succeeds does creation publish the session and agent and allow the driver to start.

A setup failure rolls back the private child. No observer can acquire a child whose first prompt used the deployment persona or unfiltered tool set and whose later prompts use the requested configuration.

Visibility is not authority

These controls compose trusted same-process behavior; they do not authorize it. toolFilter changes the child view resolved by the tool registry, but it does not create a parent-to-child grant lattice, require a child to be a subset of its parent, sandbox plugins, or prevent code with another Cordis context from calling services directly.

In particular, a child-local tool is added after the global filter and may be absent from the parent's view. A deny-only child also sees later global tools not named by the deny-list. Those are deliberate live-composition semantics, not non-escalation guarantees.

A security design would need a separate authority representation, propagation rule, and execution-time enforcement point. Creation-time grant snapshots, parent-subset grants, explicit future-grant APIs, and generic capability/output/termination tags are outside this feature.

Alternatives considered

Create one provider per persona or tool set. This multiplies providers that share the same transport and lifecycle implementation, makes dynamic deployment configuration awkward, and still needs a recursion mechanism. Providers remain about execution transport; requests carry per-child composition.

Copy the parent's complete tool view. Registration scope is flat by design, and lifetime ownership does not imply visibility inheritance. Copying a resolved view would also freeze dynamic global registrations and conflate composition with authority without defining either contract fully.

Snapshot allowed global tools at child creation. A frozen allow-set makes future registration uniformly unavailable, but it changes hot-registration semantics and starts an authorization design. The implemented filter stays a live registry predicate and documents allow-versus-deny behavior directly.

Hide only tool schemas. Presentation-only filtering lets the model execute a tool that the prompt says does not exist through Code Mode or a forged call. One resolver governs both presentation and execution instead.

Encode the depth cap as an automatic tool filter. A creation-time filter snapshots a decision that may depend on runtime state, affects only one configured tool name, and does not protect direct service callers or alternate delegation tools. The provider instead enforces the absolute cap at every start.

Consequences

Contributors can configure child role, visible global tools, and recursion without defining new providers. Capability checks fail before ownership starts, unpublished setup makes the first request consistent, and one tool resolver prevents presentation/execution drift.

The cost is that deployments must understand live allow/deny behavior and the distinction between visibility and authority. A model may call a visible delegation tool after the current depth policy forbids another child and receive an error. Provider authors must advertise each supported control accurately, and in-process providers must install every requested contribution before publication. The controls deliberately do not solve security confinement or parent-to-child non-escalation.

中文

问题

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

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

决策

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

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

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

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

人设是有作用域的遮蔽

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

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

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

<a id="tool-filtering-is-one-live-global-view-rule"></a>

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

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

解析遵循以下规则:

  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 加三代后代:JSON-RPC 示例采用这项通用策略,而 ACP 与 headless 示例固定为一。提供方缺少 depthLimit 时,数值工具上限会在提供方挂载阶段失败。

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

能力门控保持提供方诚实

能力将请求的功能与提供方实现分离。SubagentCapabilities 声明 personatoolFilterdepthLimitSubagentRuntime.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 后,模型仍可能调用可见的委派工具并收到错误。提供方作者必须准确声明每个受支持的控制,进程内提供方必须在发布前安装所有请求的贡献。这些控制有意不解决安全隔离或父到子的非升权问题。