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

This note retains the depth policy and the persona/tool-filter semantics needed to restore historical child descriptors. New children use preset-owned composition; new requests do not accept persona or tool filters. Historical overrides are applied while a resumed child is 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-prefix
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 and direct SubagentRuntime callers expose maxDepth. Its provider capability determines whether a backend can enforce the cap. Persona and tool-filter values come only from historical descriptors during cold resume.

Persona is a scoped shadow

A historical persona changes one resumed child without changing deployment-wide prompt assembly. During unpublished setup, the manager registers a child-scoped section named deployment:persona-prefix; ordinary most-specific-wins resolution replaces the inherited section only in that child’s assemblies.

The value has the same strict template semantics as the deployment persona. Omitting it preserves the composed persona; an explicit empty string shadows that persona with an empty section.

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.

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

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 PTC mode SDK generation. Independently registered system-prompt sections remain owned by their plugins. The filesystem, search, and web tool plugins use the existing PromptSection.text({ scope }) callback and ctx.tools.get(name, scope) to omit guidance for unavailable tools and select applicable cross-tool text. This keeps the original wording and ordering for a supported tool set and works for any agent scope, including underlying PTC capabilities whose wire presentation is run_code. It adds no section-ownership metadata or assembly pass; unrelated static prose is not automatically rewritten by restrict().

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; dsh-tool-subagent instead reads the live Host depth setting (initially 1), 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. The base profile uses the Host setting. A numeric tool cap fails at provider mount when the provider lacks depthLimit.

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 preset can omit delegation tool plugins independently of the depth policy. At the cap, the tool contributes an optional runtime-context notice telling the model to work directly. includeRuntimeContext: false suppresses this advisory notice; the runtime depth guard remains active. When enabled, the notice reads the live cap and durable depth at request time to keep resumed and reconfigured agents informed without changing their system prompt or tool schemas; unchanged context is not repeatedly appended.

Capability gating keeps providers honest

Capabilities separate a requested feature from a provider implementation. SubagentCapabilities.depthLimit advertises depth enforcement; SubagentRuntime.startActivation() checks a requested cap 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 manager supplies a setup callback to agent creation that composes the preset, restores historical persona and tool restrictions on cold resume, and installs structured output. 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 and still needs a recursion mechanism. Providers remain about execution transport; presets own new child composition, while descriptors retain historical overrides.

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 PTC 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

Historical child composition survives cold resume 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)后端不能默默接受一个仅限进程内的工具过滤器,而过滤器在所有插件运行于同一可信进程的情况下也不应被描述为安全边界。

决策

本记录保留深度策略,以及恢复历史子级 descriptor 所需的 persona/工具过滤语义。新子级使用 preset 拥有的组装;新请求不接受 persona 或工具过滤。历史覆盖在恢复的子级尚未发布时应用。

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

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

dsh-tool-subagent 和直接调用 SubagentRuntime 的调用方公开 maxDepth。对应的提供方能力决定后端是否能强制执行上限。persona 和工具过滤值仅在冷恢复时来自历史 descriptor。

人设是有作用域的遮蔽

历史 persona 改变一个恢复子级的行为,而不改变部署级提示词组装。在未发布的设置阶段,管理器注册子级作用域中名为 deployment:persona-prefix 的段落;普通的最具体者优先解析规则只在该子级的组装中替换继承段落。

该值与部署 persona 具有相同的严格模板语义。省略时保留组装的 persona;显式空字符串则以空段落遮蔽该 persona。

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

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

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

工具过滤同时控制能力可见性和可执行查找。进程内提供方在发布前于子 agent 作用域中安装 ToolRuntime.restrict(),注册表的单一解析器对协议格式(wire format)的工具 schema、查找、执行和 PTC mode SDK 生成施加相同的结果。独立注册的系统提示词段落仍由各插件负责。文件系统、搜索和 Web 工具插件使用已有的 PromptSection.text({ scope }) 回调与 ctx.tools.get(name, scope),省略不可用工具的指导,并选择适用的跨工具文本。这会保留受支持工具集合下的原有措辞与顺序,适用于任意 agent scope,也包括协议呈现为 run_code 的底层 PTC 能力。该方式不新增段落归属元数据或组装步骤;restrict() 不会自动改写其他静态文字。

解析遵循以下规则:

  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 可以省略上限,让此机制不约束深度;dsh-tool-subagent 则读取实时 Host 深度设置(初始值为 1)、接受数值覆盖,并使用显式的 'provider-managed' 来省略由进程外提供方部署拥有递归预算时的上限。base profile采用 Host 设置。提供方缺少 depthLimit 时,数值工具上限会在提供方挂载阶段失败。

数值上限不会合成过滤器。委派工具在上限处仍然可见,因为授权可能依赖运行时状态;每次尝试启动都会检查调用方 agent 当前的持久与运行时深度,被拒绝的启动返回错误工具结果,且不发布子 agent。preset 可独立于深度策略省略委派工具插件。达到上限时,工具贡献一条可选的运行时上下文提示,让模型直接完成工作。includeRuntimeContext: false 会抑制这条建议性提示;运行时的深度拦截仍然生效。提示启用时,每次请求读取实时上限与持久深度,使恢复会话或调整配置后的 agent 获知当前限制,同时不改变其系统提示词或工具 schema;未变化的上下文不会重复追加。

能力门控保持提供方诚实

能力将请求的功能与提供方实现分离。SubagentCapabilities.depthLimit 声明深度强制执行能力;SubagentRuntime.startActivation() 在调用提供方前检查请求的上限。

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

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

所有子级局部组装均在子级可观察前完成。管理器向 agent 创建提供设置回调,组装 preset,在冷恢复时恢复历史 persona 和工具限制,并安装结构化输出。只有设置成功后才发布会话和 agent,并允许驱动器启动。

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

可见性不是授权

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

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

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

曾考虑的替代方案

为每种 persona 或工具集创建一个提供方。 这会使共享同一传输和生命周期实现的提供方成倍增加,且仍需要递归机制。提供方负责执行传输;preset 拥有新子级组装,descriptor 保留历史覆盖。

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

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

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

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

后果

历史子级组装在冷恢复后保留,无需定义新的提供方。能力检查在所有权开始前失败,未发布设置使第一次请求一致,单一工具解析器防止呈现与执行不一致。

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