fix(subagent): resolve DSH defaults before preflight

This commit is contained in:
pku-xht
2026-08-24 21:38:28 +08:00
parent 54e908df52
commit 3c79979d1d
26 changed files with 107 additions and 59 deletions
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md
2026-08-18-model-selected-subagent-routes.md: 0802230a537d7dc701928c2f5b8f9d8152f967e3
2026-08-18-model-selected-subagent-routes.zh.md: 6a9974a05a1c7882e76acf802896b15671fd19ed
2026-08-18-model-selected-subagent-routes.md: 4542b4e66d97b21b5d557678ff2b4371d93d24f4
2026-08-18-model-selected-subagent-routes.zh.md: 48e2b6e8733a79e63fa13e2289cddec27865c016
@@ -14,9 +14,9 @@ The model also needs a bounded way to discover live providers and model-owned ef
`dsh-tool-subagent` exposes optional `provider`, `model`, and `reasoning_effort` fields only when its instance enables `enableModelSelection`, or its Agent-scoped `modelSelectionSettings` instance resolves an enabled Session decision, and the bound subagent provider advertises `SubagentCapabilities.agentOptions`. No route allowlist is required. Registered LLM provider routes are available for child selection; this tool does not add a second authorization policy over the deployment's LLM registry. Disabled instances omit and reject model-facing selection, while configured `Config.agentOptions` remain deployment-owned defaults. Either selection mode against a provider without the capability fails the plugin mount.
Provider and model form one route and must be supplied together. An effort may be supplied alone when configured or parent values provide the effective route. Model arguments override `Config.agentOptions`, and configured fields override the parent Agent's latest logged request selection; creation options supply the fallback before its first request and retain the configured output-token limit. Reasoning-effort identifiers remain adapter-owned. An unchanged route inherits an omitted effort, while changing provider or model without naming an effort clears the lower layer's route-owned value so the selected model resolves its own default. `AgentOptions` carries the resulting effort into the child loop, whose request header logs the effective value. A continuable descriptor records it with the resolved provider and model so a child that has not logged its first request can cold-resume with the same selection.
Provider and model form one route and must be supplied together. An effort may be supplied alone when configured, parent, or provider-owned values provide the effective route. Model arguments override `Config.agentOptions`. A provider with `resolveAgentOptions()` then materializes its own missing one-shot defaults; otherwise compatible missing fields come from the parent Agent's latest logged request selection, with creation options supplying the fallback before its first request and retaining the configured output-token limit. Reasoning-effort identifiers remain adapter-owned. An unchanged route inherits an omitted effort, while changing provider or model without naming an effort clears the lower layer's route-owned value so the selected model resolves its own default. `AgentOptions` carries the resulting effort into the child loop, whose request header logs the effective value. A continuable descriptor records it with the resolved provider and model so a child that has not logged its first request can cold-resume with the same selection.
An explicit or configured provider, model, or effort resolves through `ctx.llm.resolveCallConfig()` before child creation. That lookup owns provider registration, exact-model metadata, reasoning-effort validation, and adapter defaults. The tool checks cancellation again after the asynchronous lookup and before creating a child or background job. Calls with no model-facing selection and no configured route fields preserve the existing provider path without requiring the optional LLM service.
An explicit or configured provider, model, or effort first passes through the bound provider's optional synchronous default resolver, then resolves through `ctx.llm.resolveCallConfig()` before child creation. The same resolved Agent options are passed to `start()`, so parent preflight and provider execution cannot choose different routes. The LLM lookup owns provider registration, exact-model metadata, reasoning-effort validation, and adapter defaults. The tool checks cancellation again after the asynchronous lookup and before creating a child or background job. Calls with no model-facing selection and no configured route fields preserve the existing provider path without requiring the optional LLM service.
An enabled definition registers `list_subagent_models`. With no arguments the tool lists registered providers; with `provider` it calls that adapter's advisory model catalog; with `provider` and `model` it resolves the exact model and returns its reasoning efforts and default. At most one instance in a tool scope enables selection because the discovery name is global. Shipped product compositions put `modelSelectionSettings: true` on the primary Agent-scoped `subagent` instance and register the Host-owned `subagent-model-selection` settings namespace with `enabled: false`. A new top-level Session samples that preference during composition and logs an enabled decision as `subagent/model-selection-enabled` before any model request. A child Session inherits the live parent's decision, and a resumed Session uses its existing marker instead of the current preference. Therefore a settings edit affects only subsequently composed top-level Sessions. The fixed discovery definition remains available without the optional LLM service, while discovery and selected-route calls fail until that service is present. An unlisted model remains selectable when the adapter accepts its id.
@@ -24,7 +24,7 @@ Shipped `subagent_fork` instances leave `enableModelSelection` disabled even tho
The delegation definition is static across adapter registration and catalog changes, so live topology neither expands every parent request nor invalidates its cache prefix. The discovery result enters the transcript only when called. A custom inheritance-capable instance that enables selection warns that changing provider or model can prevent provider-side reuse of the inherited conversation prefix.
`SubagentCapabilities.agentOptions` remains the transport truth. The service rejects a request carrying those options before calling a provider that advertises `false`. Both in-process providers and the DSH SDK transport advertise `true`; DSH SDK merges the four supported route fields over its instance defaults and validates them during the new child runtime's `initialize`. ACP, Codex, and Claude Code advertise `false`. Tool configuration that supplies `agentOptions`, statically enables model selection, or makes it settings-controlled also fails when its bound provider lacks the capability.
`SubagentCapabilities.agentOptions` remains the transport truth. The service rejects a request carrying those options before calling a provider that advertises `false`. Both in-process providers and the DSH SDK transport advertise `true`; DSH SDK exposes its instance-default resolver, merges the four supported route fields once for tool preflight and direct starts, and validates the result during the new child runtime's `initialize`. ACP, Codex, and Claude Code advertise `false`. Tool configuration that supplies `agentOptions`, statically enables model selection, or makes it settings-controlled also fails when its bound provider lacks the capability.
## Alternatives considered
@@ -51,7 +51,7 @@ The delegation definition is static across adapter registration and catalog chan
- An enabled delegation tool can select any live child LLM route without deployment selector configuration; disabled instances omit and reject model-facing route fields.
- The primary delegation-tool instance defaults selection off, exposes a Models-page opt-in for new Sessions, and registers `list_subagent_models` only in Sessions whose durable decision is enabled; its catalog rows do not restrict delegation.
- Shipped fork tools inherit the parent's provider and model and omit model-facing route fields so the inherited conversation prefix remains eligible for KV Cache reuse.
- Omission retains configured defaults and compatible inheritance from the parent's latest logged request; a route change without an explicit effort uses the selected model's default.
- Omission retains configured defaults plus the bound provider's own defaults or compatible parent inheritance; a route change without an explicit effort uses the selected model's default.
- Adapter catalog and topology changes leave the delegation definition and its prompt-cache prefix unchanged.
- DSH SDK children accept configured and model-selected Agent routes; ACP, Codex, and Claude Code reject them until they implement and advertise the capability.
- Unit coverage owns the default-off Host preference, new-Session sampling, child inheritance, resumed decisions, opt-in schema and execution enforcement, merge precedence, route-aware effort inheritance, preflight cancellation, live discovery, diagnostics, definition stability, capability rejection, and optional-service behavior. A shipped headless snapshot pins inheritance from a logged parent selection; the shipped examples own the assembled keyless model-visible schemas, and the SDK Loader and snapshot evidence pin the complete route through a separate child runtime.
@@ -14,9 +14,9 @@ Status: implemented
只有实例启用 `enableModelSelection`,或其 Agent 作用域的 `modelSelectionSettings` 实例解析出已启用的 Session 决定,且绑定的 subagent 提供方声明 `SubagentCapabilities.agentOptions` 时,`dsh-tool-subagent` 才公开可选的 `provider``model``reasoning_effort` 字段,不要求配置路由允许列表。已注册的 LLM 提供方路由都可供子级选择;本工具不会在部署的 LLM 注册表之上增加第二套授权策略。禁用的实例会省略并拒绝面向模型的选择,而配置的 `Config.agentOptions` 仍是部署方所有的默认值。如果提供方缺少该能力,任一种选择模式都会使插件挂载失败。
提供方与模型共同组成一条路由,必须一起提供。如果配置值父级值能够提供生效路由,则可以只提供推理强度。模型参数覆盖 `Config.agentOptions`,配置字段覆盖父 Agent 最新记录的请求选择首个请求之前由创建选项提供回退,并保留其中配置的输出 token 上限。推理强度 ID 仍由 adapter 所有。路由不变时会继承省略的强度;更换提供方或模型但没有指定强度时,会清除下层路由自有的值,使所选模型解析自己的默认值。`AgentOptions` 把结果强度传入子级循环,其请求 header 会记录生效值。可继续描述符会把它与解析后的提供方和模型一同记录,使尚未写入首个请求的子级能以相同选择冷恢复。
提供方与模型共同组成一条路由,必须一起提供。如果配置值父级值或提供方自有值能够提供生效路由,则可以只提供推理强度。模型参数覆盖 `Config.agentOptions`。实现 `resolveAgentOptions()` 的提供方随后会填入自身缺失的一次性默认值;否则兼容的缺失字段来自父 Agent 最新记录的请求选择首个请求之前由创建选项提供回退,并保留其中配置的输出 token 上限。推理强度 ID 仍由 adapter 所有。路由不变时会继承省略的强度;更换提供方或模型但没有指定强度时,会清除下层路由自有的值,使所选模型解析自己的默认值。`AgentOptions` 把结果强度传入子级循环,其请求 header 会记录生效值。可继续描述符会把它与解析后的提供方和模型一同记录,使尚未写入首个请求的子级能以相同选择冷恢复。
显式或配置的提供方、模型或强度会在创建子级前通过 `ctx.llm.resolveCallConfig()` 解析。查询负责提供方注册、精确模型元数据、推理强度校验和 adapter 默认值。异步查询完成后、创建子级或后台 job 之前,工具会再次检查取消状态。既没有面向模型的选择、也没有配置路由字段的调用会保留原有提供方路径,不要求可选 LLM 服务存在。
显式或配置的提供方、模型或强度会先经过绑定提供方可选的同步默认值解析器,再在创建子级前通过 `ctx.llm.resolveCallConfig()` 解析。同一份已解析 Agent 选项会传给 `start()`,因此父级预检与提供方执行不会选择不同路由。LLM 查询负责提供方注册、精确模型元数据、推理强度校验和 adapter 默认值。异步查询完成后、创建子级或后台 job 之前,工具会再次检查取消状态。既没有面向模型的选择、也没有配置路由字段的调用会保留原有提供方路径,不要求可选 LLM 服务存在。
启用的定义会注册 `list_subagent_models`。无参数调用列出已注册提供方;提供 `provider` 时调用该适配器的建议性模型目录;同时提供 `provider``model` 时解析精确模型,并返回其推理强度和默认值。因为发现工具使用全局名称,一个工具作用域最多由一个实例启用选择。随附产品组合在 Agent 作用域的主 `subagent` 实例上设置 `modelSelectionSettings: true`,并注册默认 `enabled: false` 的 Host 自有 `subagent-model-selection` settings namespace。新的顶层 Session 会在组合期间读取该偏好,并在任何模型请求之前把启用决定记录为 `subagent/model-selection-enabled`。子 Session 继承在线父级的决定;恢复的 Session 使用已有标记,而不是当前偏好。因此,设置修改只影响之后组合的顶层 Session。即使缺少可选 LLM 服务,固定发现定义仍保持可用;发现调用和所选路由调用会在该服务出现前失败。只要适配器接受某个未列出的模型 ID,仍可选择该模型。
@@ -24,7 +24,7 @@ Status: implemented
委派定义不会随 adapter 注册和目录变化而改变,因此实时拓扑既不会扩大每个父级请求,也不会使缓存前缀失效。只有调用发现工具时,目录结果才进入 transcript。自定义的上下文继承实例如果启用选择,其描述会警告,更改提供方或模型可能阻止提供方复用继承的对话前缀。
`SubagentCapabilities.agentOptions` 仍是传输事实。如果请求携带这些选项,而提供方声明为 `false`,服务会在调用提供方前拒绝。两个进程内提供方与 DSH SDK 传输声明为 `true`DSH SDK 会把四个受支持的路由字段合并到实例默认值之上,并在新子运行时的 `initialize` 期间校验。ACP、Codex 与 Claude Code 声明为 `false`。工具配置提供 `agentOptions`、静态启用模型选择或让它受 settings 控制时,如果绑定的提供方缺少该能力,也会失败。
`SubagentCapabilities.agentOptions` 仍是传输事实。如果请求携带这些选项,而提供方声明为 `false`,服务会在调用提供方前拒绝。两个进程内提供方与 DSH SDK 传输声明为 `true`DSH SDK 会公开实例默认值解析器,为工具预检和直接启动只合并一次四个受支持的路由字段,并在新子运行时的 `initialize` 期间校验结果。ACP、Codex 与 Claude Code 声明为 `false`。工具配置提供 `agentOptions`、静态启用模型选择或让它受 settings 控制时,如果绑定的提供方缺少该能力,也会失败。
## 考虑过的替代方案
@@ -51,7 +51,7 @@ Status: implemented
- 启用的委派工具无需部署选择器配置,即可选择任意实时子级 LLM 路由;禁用的实例会省略并拒绝面向模型的路由字段。
- 主委派工具实例默认关闭选择,为新 Session 提供 Models 页面 opt-in,并且只在持久决定已启用的 Session 中注册 `list_subagent_models`;其目录条目不会限制委派。
- 随附 fork 工具会继承父级的提供方与模型,并省略面向模型的路由字段,使继承的对话前缀仍可供 KV Cache 复用。
- 省略选择时保留配置默认值,并父级最新记录请求中进行兼容继承;改变路由但不显式指定强度时,使用所选模型的默认值。
- 省略选择时保留配置默认值,并使用绑定提供方自身的默认值或来自父级最新记录请求兼容继承;改变路由但不显式指定强度时,使用所选模型的默认值。
- adapter 目录和拓扑变化不会改变委派定义及其 prompt 缓存前缀。
- DSH SDK 子级接受配置和模型选择的 Agent 路由;ACP、Codex 与 Claude Code 在实现并声明该能力前仍会拒绝。
- 单元测试覆盖默认关闭的 Host 偏好、新 Session 读取、子级继承、恢复决定、选择启用时的 schema 与执行强制、合并优先级、路由相关强度继承、预检取消、实时发现、诊断、定义稳定性、能力拒绝与可选服务行为。随附的 headless 快照固定从父级已记录选择继承的行为;随附示例覆盖组装后无密钥、模型可见的 schema,SDK Loader 与快照证据固定完整路由经过独立子运行时的链路。
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/subsystems/subagent.md
subagent.md: b9ddca3c7230d4f5adb4bae9e1b258a3b1184075
subagent.zh.md: 47a3378718c4cc5c43b44cdd5869eec3cf37f93a
subagent.md: c6017dc8621f9a4bc4c56466d06bc37e55ae3db0
subagent.zh.md: fe0b31cb00a4b90605f557d2cf5c922f790d85d4
+11 -1
View File
@@ -420,7 +420,7 @@ A local one-shot run MUST publish an ordinary child agent/session before `start(
## The provider contract: `SubagentProvider`
Each provider is a named child-agent transport, and multiple providers may coexist. The service validates requested start-time capabilities before `start()`, and rejects a continuable start on a provider without `prepareContinuable`. `inheritsParentContext` describes only conversation seeding (`fork`: true; `spawn` and `acp`: false), allowing consumers to generate accurate model-facing wording without implying inherited tools, services, or authority.
Each provider is a named child-agent transport, and multiple providers may coexist. The service validates requested start-time capabilities before `start()`, and rejects a continuable start on a provider without `prepareContinuable`. `inheritsParentContext` describes only conversation seeding (`fork`: true; `spawn` and `acp`: false), allowing consumers to generate accurate model-facing wording without implying inherited tools, services, or authority. A provider whose one-shot route has provider-owned defaults exposes the optional synchronous `resolveAgentOptions()` hook, allowing a Consumer to preflight the exact value that `start()` will apply.
```ts type-equiv
/**
@@ -442,6 +442,16 @@ interface SubagentProvider {
* It says nothing about tool registration, injected services, or authority inheritance.
*/
readonly inheritsParentContext: boolean
/**
* OPTIONAL provider-owned resolution for one-shot Agent options. A Consumer
* that preflights a selected route calls this synchronously and passes the
* returned value unchanged to {@link start}; direct callers remain valid
* because the provider applies the same resolution inside `start`.
* Implementations must be pure and declare `capabilities.agentOptions`.
* @param requested - request/config fields before provider-owned defaults.
* @returns the exact Agent options this provider will apply.
*/
resolveAgentOptions?(requested: AgentOptions | undefined): AgentOptions | undefined
/**
* Establish a ONE-SHOT child and return its handle after publication.
* The service has already validated that every requested start-time
+11 -1
View File
@@ -424,7 +424,7 @@ interface SubagentRun {
## 提供方约定:`SubagentProvider`
每个提供方都是一个具名的子 agent 传输层,多个提供方可以共存。服务在 `start()` 之前校验请求的启动时能力,并拒绝在没有 `prepareContinuable` 的提供方上发起可继续 start。`inheritsParentContext` 仅描述对话种子注入(`fork`true`spawn` 和 `acp`:false),使消费方能生成准确的面向模型措辞,而不暗示继承了工具、服务或权限。
每个提供方都是一个具名的子 agent 传输层,多个提供方可以共存。服务在 `start()` 之前校验请求的启动时能力,并拒绝在没有 `prepareContinuable` 的提供方上发起可继续 start。`inheritsParentContext` 仅描述对话种子注入(`fork`true`spawn` 和 `acp`:false),使消费方能生成准确的面向模型措辞,而不暗示继承了工具、服务或权限。如果某个提供方的一次性路由拥有提供方自有默认值,它会公开可选的同步 `resolveAgentOptions()` 钩子,使 Consumer 能够预检 `start()` 将实际应用的确切值。
```ts type-equiv
/**
@@ -446,6 +446,16 @@ interface SubagentProvider {
* It says nothing about tool registration, injected services, or authority inheritance.
*/
readonly inheritsParentContext: boolean
/**
* OPTIONAL provider-owned resolution for one-shot Agent options. A Consumer
* that preflights a selected route calls this synchronously and passes the
* returned value unchanged to {@link start}; direct callers remain valid
* because the provider applies the same resolution inside `start`.
* Implementations must be pure and declare `capabilities.agentOptions`.
* @param requested - request/config fields before provider-owned defaults.
* @returns the exact Agent options this provider will apply.
*/
resolveAgentOptions?(requested: AgentOptions | undefined): AgentOptions | undefined
/**
* Establish a ONE-SHOT child and return its handle after publication.
* The service has already validated that every requested start-time
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/user/guide/python-sdk.md
python-sdk.md: b1c7cbff744adf727b4b98048905bf81a02d5e22
python-sdk.zh.md: d3255352159eb4eb709244906c2068c0d56fcfa9
python-sdk.md: 388b259f0adbba11b7d359fcf861980cf0a3bec7
python-sdk.zh.md: 2cc23e5cd1d7d7df5ad4b27441c54e6c3239c917
-1
View File
@@ -90,7 +90,6 @@ dsh_home = Path("/absolute/path/to/example-dsh-home").resolve()
with DeepSeekHarness(
provider="deepseek-official",
model="deepseek-v4-flash",
reasoning_effort="max",
max_tokens=49_152,
cwd=str(workspace),
dsh_home=str(dsh_home),
-1
View File
@@ -90,7 +90,6 @@ dsh_home = Path("/absolute/path/to/example-dsh-home").resolve()
with DeepSeekHarness(
provider="deepseek-official",
model="deepseek-v4-flash",
reasoning_effort="max",
max_tokens=49_152,
cwd=str(workspace),
dsh_home=str(dsh_home),
@@ -19,10 +19,8 @@
profile: sdk
patches: !!js JSON.parse(process.env.DSH_TEST_CHILD_PATCHES ?? '[]')
dshHome: !!js process.env.DSH_TEST_CHILD_HOME
# These defaults are intentionally unavailable in the child composition;
# the model-selected route must replace them before initialize.
provider: unavailable-default
model: unavailable-default
provider: mock
model: mock-routed
env:
DSH_TELEMETRY_DISABLED: '1'
@@ -29,12 +29,13 @@ class MockDelegatingAdapter extends LlmAdapter {
.join('') ?? ''
if (toolResultText.length === 0) {
const selectedRoute = process.env.DSH_TEST_CHILD_DEFAULT_ROUTE === '1'
? { reasoning_effort: 'max' }
: { provider: 'mock', model: 'mock-routed', reasoning_effort: 'max' }
const args = JSON.stringify({
description: 'route probe',
prompt: 'report your route and workspace',
provider: 'mock',
model: 'mock-routed',
reasoning_effort: 'max',
...selectedRoute,
})
yield { type: 'block-start', index: 0, blockType: 'tool-call' }
yield { type: 'tool-call-delta', index: 0, id: CallId('call-delegate'), name: 'subagent', argumentsDelta: args }
@@ -4971,7 +4971,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
},
{
name: 'SubagentProvider',
declaration: 'export interface SubagentProvider {\n readonly name: string;\n readonly capabilities: SubagentCapabilities;\n readonly inheritsParentContext: boolean;\n start(request: ResolvedSubagentStartRequest): Promise<SubagentRun>;\n prepareContinuable?(request: ContinuableCreateRequest): Promise<ContinuableCreateSpec>;\n}',
declaration: 'export interface SubagentProvider {\n readonly name: string;\n readonly capabilities: SubagentCapabilities;\n readonly inheritsParentContext: boolean;\n resolveAgentOptions?(requested: AgentOptions | undefined): AgentOptions | undefined;\n start(request: ResolvedSubagentStartRequest): Promise<SubagentRun>;\n prepareContinuable?(request: ContinuableCreateRequest): Promise<ContinuableCreateSpec>;\n}',
},
{
name: 'SubagentReportDelivery',
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent-dsh-sdk/README.md
README.md: fa715e8deed5bea81e7601510a20883df9ae90e1
README.zh.md: 953fe0943e5bf5b61be49273c57c25b6e020c0d9
README.md: e91af6de8442dbeeda3b8471bc5a1075f27e8c80
README.zh.md: bce793a2118c51237a086a546c42325f573e9f2c
+1 -1
View File
@@ -20,7 +20,7 @@ The SDK client returns an owned child activity rather than a prompt result. The
## Capabilities and context
The provider advertises `agentOptions: true`, with `outputSchema`/`depthLimit`/`toolFilter`/`persona` false, and `inheritsParentContext: false`. Agent route values cross the SDK wire as an explicit whitelist; the child remains a fresh runtime in another process, and the only value derived from the parent Agent itself is the workspace cwd. `dsh-tool-subagent` deployments over this provider set `maxDepth: 'provider-managed'` — the child harness owns its own recursion budget.
The provider advertises `agentOptions: true`, with `outputSchema`/`depthLimit`/`toolFilter`/`persona` false, and `inheritsParentContext: false`. Its synchronous `resolveAgentOptions()` materializes the instance route before `dsh-tool-subagent` preflights it; `start()` applies the same resolution for direct callers, so parent validation and child initialization use one effective value. Agent route values cross the SDK wire as an explicit whitelist; the child remains a fresh runtime in another process, and the only value derived from the parent Agent itself is the workspace cwd. `dsh-tool-subagent` deployments over this provider set `maxDepth: 'provider-managed'` — the child harness owns its own recursion budget.
## Configuration
@@ -20,7 +20,7 @@ SDK 客户端返回自有子活动,而不是提示词结果。提供方读取
## 能力与上下文
提供方声明 `agentOptions: true`,同时保持 `outputSchema`/`depthLimit`/`toolFilter`/`persona` 为 false,并且 `inheritsParentContext: false`。Agent 路由值通过显式白名单跨越 SDK 协议;子进程仍是另一进程里的全新运行时,唯一从父 Agent 本身派生的值是工作区 cwd。基于本提供方的 `dsh-tool-subagent` 部署应设置 `maxDepth: 'provider-managed'`——子 harness 拥有自己的递归预算。
提供方声明 `agentOptions: true`,同时保持 `outputSchema`/`depthLimit`/`toolFilter`/`persona` 为 false,并且 `inheritsParentContext: false`同步的 `resolveAgentOptions()` 会在 `dsh-tool-subagent` 预检前填入实例路由;`start()` 对直接调用方应用同一解析,因此父级校验与子运行时初始化使用同一个生效值。Agent 路由值通过显式白名单跨越 SDK 协议;子进程仍是另一进程里的全新运行时,唯一从父 Agent 本身派生的值是工作区 cwd。基于本提供方的 `dsh-tool-subagent` 部署应设置 `maxDepth: 'provider-managed'`——子 harness 拥有自己的递归预算。
## 配置
@@ -112,10 +112,10 @@ const SDK_START_CAPABILITIES: SubagentCapabilities = Object.freeze({
})
/** Merge the request's supported route fields over this provider instance's defaults. */
function resolveSdkRoute(config: ResolvedConfig, requested: AgentOptions | undefined): Pick<
SdkRunSpec,
'provider' | 'model' | 'reasoningEffort' | 'maxTokens'
> {
function resolveSdkAgentOptions(
config: ResolvedConfig,
requested: AgentOptions | undefined,
): AgentOptions & { provider: string; model: string } {
const maxTokens = requested?.maxTokens ?? config.maxTokens
return {
provider: requested?.provider ?? config.provider,
@@ -137,8 +137,12 @@ class SdkSubagentProvider implements SubagentProvider {
constructor(readonly name: string, private readonly ctx: Context, private readonly config: ResolvedConfig) {}
resolveAgentOptions(requested: AgentOptions | undefined): AgentOptions & { provider: string; model: string } {
return resolveSdkAgentOptions(this.config, requested)
}
start(request: SubagentStartRequest) {
const route = resolveSdkRoute(this.config, request.agentOptions)
const route = this.resolveAgentOptions(request.agentOptions)
const spec: SdkRunSpec = {
...this.config.dshBin === undefined ? {} : { dshBin: this.config.dshBin },
profile: this.config.profile,
@@ -62,6 +62,7 @@ describe('SDK subagent dynamic routing through a real cordis.yml', () => {
env: {
DSH_TEST_CHILD_PATCHES: JSON.stringify([childPatch]),
DSH_TEST_CHILD_HOME: childHome,
DSH_TEST_CHILD_DEFAULT_ROUTE: '1',
},
inspect: async (cwd) => {
// The child reports realpaths; canonicalize the temp workspace to match.
+2 -2
View File
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/subagent/README.md
README.md: 68ddc49197bcbd3f8eb5f362de60da33cb08c147
README.zh.md: cf434152cd6366e371eef86f0edcb08d18978c66
README.md: 9b877358806cb471b071334e9d93742f789c2c24
README.zh.md: b1fe2b27426fb38f8798aa54718da3e4253b2887
+1 -1
View File
@@ -42,7 +42,7 @@ Start-time features are advertised in `provider.capabilities` because the servic
- `toolFilter` — apply the requested child tool restriction.
- `persona` — apply a per-child persona.
Both in-process providers advertise `agentOptions`: child creation merges requested fields over the provider, model, and reasoning effort in the parent's latest logged request, falling back to its creation options before the first request and retaining its configured token limit. A route change without an explicit effort clears the inherited route-owned effort so the selected model resolves its default. Current out-of-process providers advertise it as unsupported, so configured or model-selected overrides fail before their child transport starts instead of being silently ignored.
Both in-process providers advertise `agentOptions`: child creation merges requested fields over the provider, model, and reasoning effort in the parent's latest logged request, falling back to its creation options before the first request and retaining its configured token limit. A route change without an explicit effort clears the inherited route-owned effort so the selected model resolves its default. DSH SDK also advertises the capability and implements `resolveAgentOptions()` so its provider/model/maxTokens instance defaults are materialized before the Consumer preflights the exact route; `start()` applies the same resolution for direct callers. ACP, Codex, and Claude Code advertise the capability as unsupported, so their transports reject configured or model-selected overrides instead of silently ignoring them.
Every in-process child is composed by one call, `applyChildComposition(childCtx, parent, composition)`, which joins the parent's agent-preset composition before applying the child's own persona and tool filter. The join is what gives the child its capabilities: with every model-facing row on the agent plane, a child that joined nothing would reach the model with an empty tool registry ([`dsh-agent-presets`](../../preset/agent-presets/README.md)). Taking the parent as a parameter is deliberate — it makes composing a child WITHOUT that join unrepresentable at the call sites, which is the defect the one call exists to prevent. A deployment composing no preset roster joins nothing and needs nothing: its model-facing rows sit in the host composition, where the child already resolves them through the tool registry's global layer.
+1 -1
View File
@@ -42,7 +42,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委
- `toolFilter`:应用请求的子 agent 工具限制;
- `persona`:应用每个子 agent 独立的 persona。
两个进程内提供方都会声明 `agentOptions`:创建子 agent 时,请求字段会覆盖父级最新记录请求中的提供方、模型与推理强度;首个请求之前回退到其创建选项,并保留其中配置的 token 上限。更换路由但没有显式指定强度时,会清除继承的路由所属强度,使所选模型解析自己的默认值。当前进程外提供方会声明不支持,因此配置或模型选择的覆盖会在启动子传输前失败,而不会静默忽略。
两个进程内提供方都会声明 `agentOptions`:创建子 agent 时,请求字段会覆盖父级最新记录请求中的提供方、模型与推理强度;首个请求之前回退到其创建选项,并保留其中配置的 token 上限。更换路由但没有显式指定强度时,会清除继承的路由所属强度,使所选模型解析自己的默认值。DSH SDK 也声明该能力,并实现 `resolveAgentOptions()`,在 Consumer 预检确切路由之前填入其实例持有的 providermodelmaxTokens 默认值;直接调用方进入 `start()` 时会应用同一解析。ACP、Codex 与 Claude Code 声明不支持该能力,因此它们的传输会拒绝配置或模型选择的覆盖,而不会静默忽略。
每个进程内子 agent 都通过一次 `applyChildComposition(childCtx, parent, composition)` 调用完成组装:先加入父级的 agent-preset 组合,再应用子 agent 自己的 persona 和工具限制。加入父级组合正是子 agent 获得能力的途径:所有面向模型的行都位于 agent 平面,完全没有加入任何组合的子 agent 抵达模型时会看到空的工具注册表(见 [`dsh-agent-presets`](../../preset/agent-presets/README.zh.md))。将父级作为参数是刻意设计:这让“组装子 agent 却不做该加入”在各调用点无法表达,而这正是这一次调用所要杜绝的缺陷。未组装 preset roster 的部署不加入任何组合、也不需要加入;其面向模型的行位于宿主组合中,子 agent 已能通过工具注册表的全局层解析到它们。
+10
View File
@@ -308,6 +308,16 @@ export interface SubagentProvider {
* It says nothing about tool registration, injected services, or authority inheritance.
*/
readonly inheritsParentContext: boolean
/**
* OPTIONAL provider-owned resolution for one-shot Agent options. A Consumer
* that preflights a selected route calls this synchronously and passes the
* returned value unchanged to {@link start}; direct callers remain valid
* because the provider applies the same resolution inside `start`.
* Implementations must be pure and declare `capabilities.agentOptions`.
* @param requested - request/config fields before provider-owned defaults.
* @returns the exact Agent options this provider will apply.
*/
resolveAgentOptions?(requested: AgentOptions | undefined): AgentOptions | undefined
/**
* Establish a ONE-SHOT child and return its handle after publication.
* The service has already validated that every requested start-time
@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/subagent/tool-subagent/README.md
README.md: e643442de7fa45f15a5c2bf818e2c25feb44b6c5
README.zh.md: aa6dec73c66ce6b4db525d09cd166e671dbec9dc
README.md: db84c074f314ef4f61d52587f70bb96b9b46225d
README.zh.md: 6d2cbe5ff79bdec764986eee309f8d90295d61c9
+3 -3
View File
@@ -6,7 +6,7 @@ The model-facing delegation tool over one configured `ctx.subagents` provider. C
## Provider selection and lifecycle
Each plugin instance binds one subagent transport `provider` to one `toolName`; the model cannot change that transport. Load another distinctly named instance to expose another transport. `enableModelSelection: true`, or an enabled Host preference when `modelSelectionSettings: true`, requires that provider's child `agentOptions` capability and exposes optional child LLM `provider`, `model`, and `reasoning_effort` fields without additional route configuration. A call may supply a complete provider/model pair, or only an effort when configured or parent values supply the effective route. The live adapter resolves explicit or configured routes before child creation. A call that omits every selection field uses `agentOptions` and then inherits compatible missing values from the parent's latest logged request selection, falling back to its creation options before the first request and retaining its configured `maxTokens`. Changing provider or model without naming an effort clears the lower layer's route-owned effort so the selected model resolves its default.
Each plugin instance binds one subagent transport `provider` to one `toolName`; the model cannot change that transport. Load another distinctly named instance to expose another transport. `enableModelSelection: true`, or an enabled Host preference when `modelSelectionSettings: true`, requires that provider's child `agentOptions` capability and exposes optional child LLM `provider`, `model`, and `reasoning_effort` fields without additional route configuration. A call may supply a complete provider/model pair, or only an effort when configured, parent, or provider-owned defaults supply the effective route. Model fields first override tool `agentOptions`; a provider with `resolveAgentOptions()` then materializes its own missing defaults before the live adapter preflights the exact route, and the same resolved value reaches `start()`. Providers without that hook retain compatible missing values from the parent's latest logged request selection, falling back to its creation options before the first request and retaining its configured `maxTokens`. Changing provider or model without naming an effort clears the lower layer's route-owned effort so the selected model resolves its default.
The delegation tool registers only while its subagent provider exists, avoiding sibling load-order and provider-reload dependencies. When model selection is enabled, its optional fields remain visible without `ctx.llm`; a call that selects a route rejects if the service is unavailable. When disabled, the schema omits those fields and execution rejects a forced selection. Configured `agentOptions` remain deployment-owned child defaults independently of this model-facing switch. Adapter catalog and topology changes do not rewrite or re-register the tool. Its description follows `provider.inheritsParentContext`: fresh children require standalone prompts, while forked children already see completed parent turns.
@@ -28,7 +28,7 @@ A foreground call passes the execution signal through startup and execution, awa
| `modelSelectionSettings` | Samples the Host `subagent-model-selection` preference while composing an Agent, records an enabled decision in its Session, and inherits that decision in child Sessions. Default `false`; mutually exclusive with `enableModelSelection` and valid only in an Agent-scoped composition. The preference defaults off and changes only subsequently composed top-level Sessions. |
| `enableRunInBackground` | Exposes background mode, default `true`; disabling also rejects forced background calls. |
| `backgroundMode` | Background lifecycle policy, default `one-shot`. `one-shot` defaults calls to foreground; `continuable` defaults them to background, requires the provider's `prepareContinuable` capability, and returns a durable child id without requiring the follow-up tool. |
| `agentOptions` | Configured child LLM `provider`, `model`, adapter-owned `reasoningEffort`, and positive `maxTokens`; requires the subagent provider's `agentOptions` capability. In-process providers merge explicit values over the parent's latest logged request selection, or its creation options before the first request. An inherited effort survives only while the effective provider/model route is unchanged; changing the route without an explicit effort lets the selected model supply its default. A configured provider, model, or effort is checked through the optional `ctx.llm` service before child creation even when the call omits model-selection fields; a missing service or invalid value rejects the call. |
| `agentOptions` | Configured child LLM `provider`, `model`, adapter-owned `reasoningEffort`, and positive `maxTokens`; requires the subagent provider's `agentOptions` capability. Providers may resolve their own missing defaults before preflight; otherwise in-process providers merge explicit values over the parent's latest logged request selection, or its creation options before the first request. An inherited effort survives only while the effective provider/model route is unchanged; changing the route without an explicit effort lets the selected model supply its default. A configured provider, model, or effort is checked through the optional `ctx.llm` service before child creation even when the call omits model-selection fields; a missing service or invalid value rejects the call. |
| `persona` | Per-child persona; requires provider `persona` capability. |
| `toolFilter` | Per-child global-tool restriction; requires `toolFilter` capability. |
| `maxDepth` | Absolute delegation-depth cap, default `3` (`0` forbids delegation); a numeric cap requires the `depthLimit` capability and fails the mount without it. `'provider-managed'` sends no cap for an out-of-process provider whose budget belongs to the child harness. The tool stays visible at the cap; each attempted start checks the calling agent's current depth and returns an errored tool result when rejected. |
@@ -100,4 +100,4 @@ Append-only; newly visible content follows the reusable request prefix and does
- **Background runs expose no result through this tool** — a one-shot task's final output is collected through the generic task surface, and a continuable child's output stays in its own session, read by its subagent id. The settlement notice states how that child ended and carries any final assistant message, but it is not this call's return value and cannot be awaited here.
- **Duplicate names across waiting one-shot instances are detected late** (`TODO(subagent-dup-toolname)`) — continuable instances reserve their prompt-section name during plugin application, but preventing provider-registration rollback for waiting one-shot instances requires a registry of intended names.
- **Shipped fork tools cannot select a child LLM route** — they inherit the parent's provider and model to keep the copied conversation prefix eligible for KV Cache reuse. Re-enable the fields only when route changes preserve reuse or expose a bounded recomputation cost.
- **Non-routing child policy is fixed per instance** — another persona, tool filter, or depth cap requires another distinctly named tool. LLM provider/model/reasoning-effort selection requires static enablement or an enabled per-Session preference and a subagent provider that advertises `agentOptions`; out-of-process providers currently reject enabling it rather than ignore it.
- **Non-routing child policy is fixed per instance** — another persona, tool filter, or depth cap requires another distinctly named tool. LLM provider/model/reasoning-effort selection requires static enablement or an enabled per-Session preference and a subagent provider that advertises `agentOptions`; ACP, Codex, and Claude Code reject it rather than ignore it.
+3 -3
View File
@@ -6,7 +6,7 @@
## 提供方选择与生命周期
每个插件实例把一个 subagent 传输 `provider` 绑定到一个 `toolName`;模型不能改变该传输。如需公开另一种传输,请加载另一个名称不同的实例。`enableModelSelection: true`,或 `modelSelectionSettings: true` 时已启用的 Host 偏好,都要求该提供方具备子级 `agentOptions` 能力,并且无需额外路由配置即可公开可选的子 agent LLM `provider``model``reasoning_effort` 字段。调用可以提供完整的提供方/模型对;当配置值父 Agent 值能够提供生效路由时,也可以只提供推理强度。实时 adapter 会在创建子 agent 前解析显式或配置的路由。完全省略选择字段的调用使用 `agentOptions`,再从父 Agent 最新记录的请求选择中继承兼容的缺失值;首个请求之前回退到其创建选项,并保留其中配置的 `maxTokens`。如果更换提供方或模型但没有指定强度,则清除下层路由所属的强度,使所选模型解析自己的默认值。
每个插件实例把一个 subagent 传输 `provider` 绑定到一个 `toolName`;模型不能改变该传输。如需公开另一种传输,请加载另一个名称不同的实例。`enableModelSelection: true`,或 `modelSelectionSettings: true` 时已启用的 Host 偏好,都要求该提供方具备子级 `agentOptions` 能力,并且无需额外路由配置即可公开可选的子 agent LLM `provider``model``reasoning_effort` 字段。调用可以提供完整的提供方/模型对;当配置值父 Agent 值或提供方持有的默认值能够提供生效路由时,也可以只提供推理强度。模型字段会先覆盖工具 `agentOptions`;实现 `resolveAgentOptions()` 的提供方随后会在实时 adapter 预检确切路由前填入自身缺失的默认值,同一份解析结果再进入 `start()`。没有该钩子的提供方会从父 Agent 最新记录的请求选择中保留兼容的缺失值;首个请求之前回退到其创建选项,并保留其中配置的 `maxTokens`。如果更换提供方或模型但没有指定强度,则清除下层路由所属的强度,使所选模型解析自己的默认值。
委派工具只在其 subagent 提供方存在时注册,从而避免对同级加载顺序和提供方重新加载的依赖。启用模型选择时,即使没有 `ctx.llm`,可选字段仍然可见;选择路由的调用会在该服务缺失时失败。禁用时,schema 会省略这些字段,执行阶段也会拒绝强制传入的选择。配置的 `agentOptions` 仍是部署方所有的子级默认值,不受这个面向模型的开关影响。adapter 目录和拓扑变化不会改写或重新注册工具。工具描述遵循 `provider.inheritsParentContext`:新建子 agent(智能体)需要独立提示词,而 fork 子 agent 已能看到父级已完成轮次。
@@ -28,7 +28,7 @@
| `modelSelectionSettings` | 组合 Agent 时读取 Host 的 `subagent-model-selection` 偏好,把启用决定记录进其 Session,并让子 Session 继承该决定。默认为 `false`;与 `enableModelSelection` 互斥,且只能用于 Agent 作用域组合。该偏好默认关闭,只影响之后组合的新顶层 Session。 |
| `enableRunInBackground` | 公开后台模式,默认 `true`;禁用时也会拒绝强制后台调用。 |
| `backgroundMode` | 后台生命周期策略,默认 `one-shot``one-shot` 默认前台调用;`continuable` 默认后台调用,要求提供方具备 `prepareContinuable` 能力,并返回持久化子 agent ID,且不要求加载后续消息工具。 |
| `agentOptions` | 配置的子 agent LLM `provider``model`、adapter 自有 `reasoningEffort` 与正整数 `maxTokens`;要求 subagent 提供方具备 `agentOptions` 能力。进程内提供方把显式值合并到父 Agent 最新记录的请求选择之上首个请求之前则合并到其创建选项之上。只有生效提供方/模型路由不变时才会保留继承的推理强度;改变路由但不显式提供强度时,由所选模型提供默认值。即使调用省略模型选择字段,配置的提供方、模型或强度也会在创建子 agent 前通过可选 `ctx.llm` 服务进行校验;服务缺失或值无效都会拒绝调用。 |
| `agentOptions` | 配置的子 agent LLM `provider``model`、adapter 自有 `reasoningEffort` 与正整数 `maxTokens`;要求 subagent 提供方具备 `agentOptions` 能力。提供方可以在预检前解析自身缺失的默认值;否则进程内提供方把显式值合并到父 Agent 最新记录的请求选择之上首个请求之前则合并到其创建选项之上。只有生效提供方/模型路由不变时才会保留继承的推理强度;改变路由但不显式提供强度时,由所选模型提供默认值。即使调用省略模型选择字段,配置的提供方、模型或强度也会在创建子 agent 前通过可选 `ctx.llm` 服务进行校验;服务缺失或值无效都会拒绝调用。 |
| `persona` | 每个子 agent 独立的 persona;要求提供方具备 `persona` 能力。 |
| `toolFilter` | 每个子 agent 独立的全局工具限制;要求提供方具备 `toolFilter` 能力。 |
| `maxDepth` | 绝对委派深度上限,默认 `3``0` 禁止委派);数值上限要求 `depthLimit` 能力,缺失时挂载失败。对于预算由子 harness 拥有的进程外提供方,`'provider-managed'` 不发送上限。工具在达到上限时仍然可见;每次尝试启动都会检查调用 agent 的当前深度,被拒绝时返回出错的工具结果。 |
@@ -100,4 +100,4 @@ adapter 注册和目录变化不会改变 schema 的前缀稳定性。每次结
- **后台运行不通过本工具公开结果**:一次性任务的最终输出通过通用 Task 接口收集,可继续子 agent 的输出留在其自身会话中,按其 subagent id 读取。结算通知会说明该子 agent 如何结束,并携带可能存在的最终 assistant 消息,但它不是本次调用的返回值,也无法在此等待。
- **等待中的一次性实例较晚才发现重复名称**(`TODO(subagent-dup-toolname)`):可继续实例会在插件应用期间预留提示词 section 名称,但若要阻止等待中的一次性实例回滚提供方注册,仍需要一份预期名称注册表。
- **随附 fork 工具无法选择子级 LLM 路由**:它们会继承父级的提供方与模型,使复制的对话前缀仍可供 KV Cache 复用。只有在路由变化仍能保留复用,或接口能公开一项有界的重算成本时,才重新启用这些字段。
- **每个实例的非路由子 agent 策略固定**:其他 persona、工具过滤器或深度上限都需要另一个名称不同的工具。LLM 提供方/模型/推理强度选择要求静态启用或每 Session 偏好已启用,并要求 subagent 提供方声明 `agentOptions`进程外提供方目前会拒绝启用它,而不是忽略它。
- **每个实例的非路由子 agent 策略固定**:其他 persona、工具过滤器或深度上限都需要另一个名称不同的工具。LLM 提供方/模型/推理强度选择要求静态启用或每 Session 偏好已启用,并要求 subagent 提供方声明 `agentOptions`ACP、Codex 与 Claude Code 会拒绝它,而不是忽略它。
+21 -6
View File
@@ -365,9 +365,13 @@ export function apply(ctx: Context, config: Config): void {
const mount = (subagentProvider: SubagentProvider): void => {
assertSubagentProviderConfiguration(subagentProvider)
const wording = providerWording(subagentProvider.inheritsParentContext)
const providerOwnsAgentOptionDefaults = subagentProvider.resolveAgentOptions !== undefined
const selectionDescription = providerOwnsAgentOptionDefaults
? ' Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and this provider\'s route defaults. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model\'s default effort.'
: ' Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model\'s default effort.'
const choiceDescription = !modelSelectionEnabled
? ''
: ' Child LLM selection is optional. Omit `provider`, `model`, and `reasoning_effort` to use configured child defaults and inherit compatible missing values from the parent Agent. Supply `provider` and `model` together after using `list_subagent_models` to inspect advertised routes and efforts. Changing the effective route without naming an effort uses the selected model\'s default effort.'
: selectionDescription
+ (subagentProvider.inheritsParentContext
? ' Changing the route can prevent provider-side reuse of the inherited conversation prefix.'
: '')
@@ -395,15 +399,21 @@ export function apply(ctx: Context, config: Config): void {
...modelSelectionEnabled ? {
provider: {
type: 'string' as const,
description: 'LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route.',
description: providerOwnsAgentOptionDefaults
? 'LLM provider route for the child. Supply together with model; omit both to use configured child defaults or this provider\'s route defaults.'
: 'LLM provider route for the child. Supply together with model; omit both to use configured child defaults or inherit the parent route.',
},
model: {
type: 'string' as const,
description: 'Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route.',
description: providerOwnsAgentOptionDefaults
? 'Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or this provider\'s route defaults.'
: 'Model id interpreted by provider. Supply together with provider; omit both to use configured child defaults or inherit the parent route.',
},
reasoning_effort: {
type: 'string' as const,
description: 'Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model\'s default.',
description: providerOwnsAgentOptionDefaults
? 'Adapter-owned reasoning effort for the effective child route. Omit to use a compatible configured/provider effort or the selected model\'s default.'
: 'Adapter-owned reasoning effort for the effective child route. Omit to inherit a compatible configured/parent effort or use a newly selected model\'s default.',
},
} : {},
...backgroundEnabled ? {
@@ -466,13 +476,18 @@ export function apply(ctx: Context, config: Config): void {
const modelRequest = args as DelegationModelRequest
const parentOptions = parentAgentOptionsForDelegation(parent)
const childAgentOptions = requestedAgentOptions(
const requestedChildAgentOptions = requestedAgentOptions(
parentOptions,
config.agentOptions,
modelRequest,
modelSelectionEnabled,
)
if (hasDelegationModelRequest(modelRequest) || hasConfiguredLlmSelection(config.agentOptions)) {
const requiresRoutePreflight = hasDelegationModelRequest(modelRequest)
|| hasConfiguredLlmSelection(config.agentOptions)
const childAgentOptions = requiresRoutePreflight
? subagentProvider.resolveAgentOptions?.(requestedChildAgentOptions) ?? requestedChildAgentOptions
: requestedChildAgentOptions
if (requiresRoutePreflight) {
const llm = runtimeCtx.get('llm')
if (llm === undefined) {
throw new Error('cannot resolve the selected child LLM route because the `llm` service is unavailable')
@@ -220,10 +220,8 @@ describe('dsh-tool-subagent', () => {
expect(text(result)).toContain('abnormally')
})
it('forwards configured agentOptions into the start request', async () => {
// Cover the `config.agentOptions ? … : {}` spread: a provider that captures
// the request lets us assert the agentOptions reached it.
let seen: { agentOptions?: { model?: string } } | undefined
it('preflights and starts with provider-resolved Agent options', async () => {
let seen: { agentOptions?: { provider?: string; model?: string; maxTokens?: number } } | undefined
const ctx = new Context()
await ctx.plugin(LlmRuntime)
await ctx.plugin(SystemPrompt)
@@ -233,6 +231,7 @@ describe('dsh-tool-subagent', () => {
name: 'capture',
capabilities: { agentOptions: true, outputSchema: false, depthLimit: false, toolFilter: false, persona: false },
inheritsParentContext: false,
resolveAgentOptions: requested => ({ provider: 'alpha', maxTokens: 321, ...requested }),
start: async (request) => {
seen = request
return {
@@ -246,12 +245,14 @@ describe('dsh-tool-subagent', () => {
ctx.llm.registerAdapter(['alpha'], new MockAdapter([]))
await ctx.plugin(tool, {
provider: 'capture',
agentOptions: { provider: 'alpha', model: 'child-model' },
agentOptions: { model: 'child-model' },
maxDepth: 'provider-managed',
})
await callSubagent(ctx, { description: 'd', prompt: 'p' })
expect(seen?.agentOptions).toEqual({ provider: 'alpha', model: 'child-model' })
expect(ctx.tools.schemas().find(schema => schema.name === 'subagent')?.description)
.toContain('this provider\'s route defaults')
expect(seen?.agentOptions).toEqual({ provider: 'alpha', model: 'child-model', maxTokens: 321 })
})
it('defaults toolName and omits agentOptions when apply() is called directly (schema bypass)', async () => {