diff --git a/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.i18n.yaml b/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.i18n.yaml index 720a40eb39..c90780b494 100644 --- a/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.i18n.yaml @@ -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: 4542b4e66d97b21b5d557678ff2b4371d93d24f4 -2026-08-18-model-selected-subagent-routes.zh.md: 48e2b6e8733a79e63fa13e2289cddec27865c016 +2026-08-18-model-selected-subagent-routes.md: ffaccb27de8a9735b60266bfb995227fa8a4cec9 +2026-08-18-model-selected-subagent-routes.zh.md: a102609e84edbba112d6845e86c3c4ba0254e0e6 diff --git a/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md b/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md index 4542b4e66d..ffaccb27de 100644 --- a/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md +++ b/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.md @@ -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, 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. +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. Static `provider.agentRouteDefaults`, when present, establish the provider/model/reasoning baseline; `Config.agentOptions` and model arguments overlay it before route-aware effort clearing. Providers without static defaults use compatible fields 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 only from the selected baseline; 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 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 explicit or configured provider, model, or effort resolves through `ctx.llm.resolveCallConfig()` after the provider baseline and request precedence are complete. Providers with static route defaults suppress parent-effort inheritance when the request omits effort, preserving the selected model's default. The LLM lookup owns provider registration, exact-model metadata, reasoning-effort validation, and adapter defaults. After the asynchronous lookup, the tool checks cancellation and confirms the same provider instance remains registered before creating a child or background job, so HMR cannot combine one provider's defaults with another provider's process. 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 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. +`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 publishes its provider/model defaults as detached immutable data for Consumer preflight, while `start()` independently applies the same Config defaults plus maxTokens for direct callers and child initialization. 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 plus the bound provider's own defaults or compatible parent inheritance; a route change without an explicit effort uses the selected model's default. +- Omission retains configured defaults plus static provider route 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. diff --git a/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.zh.md b/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.zh.md index 48e2b6e873..a102609e84 100644 --- a/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.zh.md +++ b/.agents/notes/implemented/feature/2026-08-18-model-selected-subagent-routes.zh.md @@ -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`。实现 `resolveAgentOptions()` 的提供方随后会填入自身缺失的一次性默认值;否则兼容的缺失字段来自父 Agent 最新记录的请求选择,首个请求之前由创建选项提供回退,并保留其中配置的输出 token 上限。推理强度 ID 仍由 adapter 所有。路由不变时会继承省略的强度;更换提供方或模型但没有指定强度时,会清除下层路由自有的值,使所选模型解析自己的默认值。`AgentOptions` 把结果强度传入子级循环,其请求 header 会记录生效值。可继续描述符会把它与解析后的提供方和模型一同记录,使尚未写入首个请求的子级能以相同选择冷恢复。 +提供方与模型共同组成一条路由,必须一起提供。如果配置值、父级值或提供方自有值能够提供生效路由,则可以只提供推理强度。静态的 `provider.agentRouteDefaults` 在存在时构成 provider/model/reasoning 基线;`Config.agentOptions` 与模型参数会在路由相关强度清除之前覆盖它。没有静态默认值的提供方会使用父 Agent 最新记录请求中的兼容字段,首个请求之前由创建选项提供回退,并保留其中配置的输出 token 上限。推理强度 ID 仍由 adapter 所有。只有所选基线的路由不变时才会继承省略的强度;更换提供方或模型但没有指定强度时,会清除下层路由自有的值,使所选模型解析自己的默认值。`AgentOptions` 把结果强度传入子级循环,其请求 header 会记录生效值。可继续描述符会把它与解析后的提供方和模型一同记录,使尚未写入首个请求的子级能以相同选择冷恢复。 -显式或配置的提供方、模型或强度会先经过绑定提供方可选的同步默认值解析器,再在创建子级前通过 `ctx.llm.resolveCallConfig()` 解析。同一份已解析 Agent 选项会传给 `start()`,因此父级预检与提供方执行不会选择不同路由。LLM 查询负责提供方注册、精确模型元数据、推理强度校验和 adapter 默认值。异步查询完成后、创建子级或后台 job 之前,工具会再次检查取消状态。既没有面向模型的选择、也没有配置路由字段的调用会保留原有提供方路径,不要求可选 LLM 服务存在。 +显式或配置的提供方、模型或强度会在提供方基线与请求优先级完成后,通过 `ctx.llm.resolveCallConfig()` 解析。具有静态路由默认值的提供方会在请求省略强度时禁止继承父级强度,从而保留所选模型的默认值。LLM 查询负责提供方注册、精确模型元数据、推理强度校验和 adapter 默认值。异步查询完成后、创建子级或后台 job 之前,工具会再次检查取消状态,并确认同一个提供方实例仍处于注册状态,因此 HMR 不会把一个提供方的默认值与另一个提供方的进程组合。既没有面向模型的选择、也没有配置路由字段的调用会保留原有提供方路径,不要求可选 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 会把 provider/model 默认值作为分离且不可变的数据公开给 Consumer 预检,而 `start()` 会为直接调用方与子运行时初始化独立应用同一份 Config 默认值及 maxTokens。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 与快照证据固定完整路由经过独立子运行时的链路。 diff --git a/docs/subsystems/subagent.i18n.yaml b/docs/subsystems/subagent.i18n.yaml index f7e8badd25..a0690c5a74 100644 --- a/docs/subsystems/subagent.i18n.yaml +++ b/docs/subsystems/subagent.i18n.yaml @@ -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: c6017dc8621f9a4bc4c56466d06bc37e55ae3db0 -subagent.zh.md: fe0b31cb00a4b90605f557d2cf5c922f790d85d4 +subagent.md: 7ec3e66c4f52dbc364cb3d85763a659602499b9d +subagent.zh.md: 158f20a06670143ed681f66c16ff6abb5de4a0cb diff --git a/docs/subsystems/subagent.md b/docs/subsystems/subagent.md index c6017dc862..7ec3e66c4f 100644 --- a/docs/subsystems/subagent.md +++ b/docs/subsystems/subagent.md @@ -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. 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. +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 static provider-owned defaults publishes optional immutable `agentRouteDefaults`, allowing a Consumer to merge model/tool overrides against the correct baseline before preflight. ```ts type-equiv /** @@ -443,15 +443,12 @@ interface SubagentProvider { */ 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. + * Optional static provider-owned route defaults for one-shot Agent options. + * Consumers merge tool/model overrides over these values before preflight; + * providers whose missing route fields derive from the parent omit it. + * The value is detached immutable data and requires `agentOptions` support. */ - resolveAgentOptions?(requested: AgentOptions | undefined): AgentOptions | undefined + readonly agentRouteDefaults?: Readonly> /** * Establish a ONE-SHOT child and return its handle after publication. * The service has already validated that every requested start-time diff --git a/docs/subsystems/subagent.zh.md b/docs/subsystems/subagent.zh.md index fe0b31cb00..158f20a066 100644 --- a/docs/subsystems/subagent.zh.md +++ b/docs/subsystems/subagent.zh.md @@ -424,7 +424,7 @@ interface SubagentRun { ## 提供方约定:`SubagentProvider` -每个提供方都是一个具名的子 agent 传输层,多个提供方可以共存。服务在 `start()` 之前校验请求的启动时能力,并拒绝在没有 `prepareContinuable` 的提供方上发起可继续 start。`inheritsParentContext` 仅描述对话种子注入(`fork`:true;`spawn` 和 `acp`:false),使消费方能生成准确的面向模型措辞,而不暗示继承了工具、服务或权限。如果某个提供方的一次性路由拥有提供方自有默认值,它会公开可选的同步 `resolveAgentOptions()` 钩子,使 Consumer 能够预检 `start()` 将实际应用的确切值。 +每个提供方都是一个具名的子 agent 传输层,多个提供方可以共存。服务在 `start()` 之前校验请求的启动时能力,并拒绝在没有 `prepareContinuable` 的提供方上发起可继续 start。`inheritsParentContext` 仅描述对话种子注入(`fork`:true;`spawn` 和 `acp`:false),使消费方能生成准确的面向模型措辞,而不暗示继承了工具、服务或权限。如果某个提供方的一次性路由拥有静态的提供方自有默认值,它会公开可选且不可变的 `agentRouteDefaults`,使 Consumer 能够在预检前以正确基线合并模型与工具覆盖。 ```ts type-equiv /** @@ -447,15 +447,12 @@ interface SubagentProvider { */ 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. + * Optional static provider-owned route defaults for one-shot Agent options. + * Consumers merge tool/model overrides over these values before preflight; + * providers whose missing route fields derive from the parent omit it. + * The value is detached immutable data and requires `agentOptions` support. */ - resolveAgentOptions?(requested: AgentOptions | undefined): AgentOptions | undefined + readonly agentRouteDefaults?: Readonly> /** * Establish a ONE-SHOT child and return its handle after publication. * The service has already validated that every requested start-time diff --git a/examples/python-sdk-agent/tests/fixtures/subagent/subagent-dsh-sdk/mock-delegating-llm.ts b/examples/python-sdk-agent/tests/fixtures/subagent/subagent-dsh-sdk/mock-delegating-llm.ts index 25074945ea..57e1d5d729 100644 --- a/examples/python-sdk-agent/tests/fixtures/subagent/subagent-dsh-sdk/mock-delegating-llm.ts +++ b/examples/python-sdk-agent/tests/fixtures/subagent/subagent-dsh-sdk/mock-delegating-llm.ts @@ -1,4 +1,5 @@ import type { Context } from '@deepseek-ai/cordis' +import { appendFileSync } from 'node:fs' import type { GenerateOptions, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm' import { CallId, LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm' @@ -10,6 +11,9 @@ import { CallId, LlmAdapter, ReasoningEffortId } from '@deepseek-ai/dsh-llm' */ class MockDelegatingAdapter extends LlmAdapter { override resolveModel(provider: string, model: string): Promise { + if (process.env.DSH_TEST_PARENT_MODEL_RECORD !== undefined) { + appendFileSync(process.env.DSH_TEST_PARENT_MODEL_RECORD, `${provider}/${model}\n`) + } return Promise.resolve({ provider, id: model, diff --git a/packages/extensions/tool-cordis/src/api-catalog.ts b/packages/extensions/tool-cordis/src/api-catalog.ts index 986966aa7c..30fd96badc 100644 --- a/packages/extensions/tool-cordis/src/api-catalog.ts +++ b/packages/extensions/tool-cordis/src/api-catalog.ts @@ -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 resolveAgentOptions?(requested: AgentOptions | undefined): AgentOptions | undefined;\n start(request: ResolvedSubagentStartRequest): Promise;\n prepareContinuable?(request: ContinuableCreateRequest): Promise;\n}', + declaration: 'export interface SubagentProvider {\n readonly name: string;\n readonly capabilities: SubagentCapabilities;\n readonly inheritsParentContext: boolean;\n readonly agentRouteDefaults?: Readonly>;\n start(request: ResolvedSubagentStartRequest): Promise;\n prepareContinuable?(request: ContinuableCreateRequest): Promise;\n}', }, { name: 'SubagentReportDelivery', diff --git a/packages/subagent/subagent-dsh-sdk/README.i18n.yaml b/packages/subagent/subagent-dsh-sdk/README.i18n.yaml index 75ee619349..fbf6210b26 100644 --- a/packages/subagent/subagent-dsh-sdk/README.i18n.yaml +++ b/packages/subagent/subagent-dsh-sdk/README.i18n.yaml @@ -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: e91af6de8442dbeeda3b8471bc5a1075f27e8c80 -README.zh.md: bce793a2118c51237a086a546c42325f573e9f2c +README.md: d9b29b594c13cd98cf4eaf3b4c8f93caf935a82c +README.zh.md: 9e1167f4d76cd93860c73e239f689512df38cb2b diff --git a/packages/subagent/subagent-dsh-sdk/README.md b/packages/subagent/subagent-dsh-sdk/README.md index e91af6de84..d9b29b594c 100644 --- a/packages/subagent/subagent-dsh-sdk/README.md +++ b/packages/subagent/subagent-dsh-sdk/README.md @@ -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`. 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. +The provider advertises `agentOptions: true`, with `outputSchema`/`depthLimit`/`toolFilter`/`persona` false, and `inheritsParentContext: false`. Its immutable `agentRouteDefaults` publish the configured provider/model baseline to `dsh-tool-subagent` before model overrides and exact-route preflight; `start()` independently applies the same Config defaults for direct callers and maxTokens. 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 diff --git a/packages/subagent/subagent-dsh-sdk/README.zh.md b/packages/subagent/subagent-dsh-sdk/README.zh.md index bce793a211..9e1167f4d7 100644 --- a/packages/subagent/subagent-dsh-sdk/README.zh.md +++ b/packages/subagent/subagent-dsh-sdk/README.zh.md @@ -20,7 +20,7 @@ SDK 客户端返回自有子活动,而不是提示词结果。提供方读取 ## 能力与上下文 -提供方声明 `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 拥有自己的递归预算。 +提供方声明 `agentOptions: true`,同时保持 `outputSchema`/`depthLimit`/`toolFilter`/`persona` 为 false,并且 `inheritsParentContext: false`。不可变的 `agentRouteDefaults` 会在模型覆盖与确切路由预检前,把配置的 provider/model 基线公开给 `dsh-tool-subagent`;`start()` 则为直接调用方与 maxTokens 独立应用同一份 Config 默认值。Agent 路由值通过显式白名单跨越 SDK 协议;子进程仍是另一进程里的全新运行时,唯一从父 Agent 本身派生的值是工作区 cwd。基于本提供方的 `dsh-tool-subagent` 部署应设置 `maxDepth: 'provider-managed'`——子 harness 拥有自己的递归预算。 ## 配置 diff --git a/packages/subagent/subagent-dsh-sdk/src/index.ts b/packages/subagent/subagent-dsh-sdk/src/index.ts index 29df6e4688..01c1a17012 100644 --- a/packages/subagent/subagent-dsh-sdk/src/index.ts +++ b/packages/subagent/subagent-dsh-sdk/src/index.ts @@ -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 resolveSdkAgentOptions( - config: ResolvedConfig, - requested: AgentOptions | undefined, -): AgentOptions & { provider: string; model: string } { +function resolveSdkRoute(config: ResolvedConfig, requested: AgentOptions | undefined): Pick< + SdkRunSpec, + 'provider' | 'model' | 'reasoningEffort' | 'maxTokens' +> { const maxTokens = requested?.maxTokens ?? config.maxTokens return { provider: requested?.provider ?? config.provider, @@ -132,17 +132,16 @@ function resolveSdkAgentOptions( */ class SdkSubagentProvider implements SubagentProvider { readonly capabilities = SDK_START_CAPABILITIES + readonly agentRouteDefaults: Readonly> // Context contract: an out-of-process SDK child starts fresh — no parent conversation crosses the process boundary. readonly inheritsParentContext = false - 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) + constructor(readonly name: string, private readonly ctx: Context, private readonly config: ResolvedConfig) { + this.agentRouteDefaults = Object.freeze({ provider: config.provider, model: config.model }) } start(request: SubagentStartRequest) { - const route = this.resolveAgentOptions(request.agentOptions) + const route = resolveSdkRoute(this.config, request.agentOptions) const spec: SdkRunSpec = { ...this.config.dshBin === undefined ? {} : { dshBin: this.config.dshBin }, profile: this.config.profile, diff --git a/packages/subagent/subagent-dsh-sdk/tests/loader-composition.e2e.ts b/packages/subagent/subagent-dsh-sdk/tests/loader-composition.e2e.ts index 63e9c1a223..639f0446f4 100644 --- a/packages/subagent/subagent-dsh-sdk/tests/loader-composition.e2e.ts +++ b/packages/subagent/subagent-dsh-sdk/tests/loader-composition.e2e.ts @@ -46,6 +46,7 @@ describe('SDK subagent dynamic routing through a real cordis.yml', () => { let events: SessionEvent[] = [] let childEvents: SessionEvent[] = [] + let parentResolvedRoutes: string[] = [] let workspace = '' try { const { stderr } = await runLoaderSmoke({ @@ -63,6 +64,7 @@ describe('SDK subagent dynamic routing through a real cordis.yml', () => { DSH_TEST_CHILD_PATCHES: JSON.stringify([childPatch]), DSH_TEST_CHILD_HOME: childHome, DSH_TEST_CHILD_DEFAULT_ROUTE: '1', + DSH_TEST_PARENT_MODEL_RECORD: '.parent-model-routes', }, inspect: async (cwd) => { // The child reports realpaths; canonicalize the temp workspace to match. @@ -79,6 +81,7 @@ describe('SDK subagent dynamic routing through a real cordis.yml', () => { const childLogs = await jsonlFiles(childSessions) expect(childLogs).toHaveLength(1) childEvents = await sessionEvents(childLogs[0] as string) + parentResolvedRoutes = (await readFile(join(cwd, '.parent-model-routes'), 'utf8')).trim().split('\n') }, }) expect(stderr).not.toContain('UNHANDLED') @@ -93,6 +96,7 @@ describe('SDK subagent dynamic routing through a real cordis.yml', () => { .map(block => block.text) .join('') expect(resultText).toBe(`child route: mock/mock-routed/max/777; cwd: ${workspace}`) + expect(parentResolvedRoutes).toContain('mock/mock-routed') // The child ran a real turn with the model-selected route and tool-configured cap. expect(childEvents.some(event => event.type === 'user/message')).toBe(true) diff --git a/packages/subagent/subagent/README.i18n.yaml b/packages/subagent/subagent/README.i18n.yaml index 4413087d00..5e65073709 100644 --- a/packages/subagent/subagent/README.i18n.yaml +++ b/packages/subagent/subagent/README.i18n.yaml @@ -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: 9b877358806cb471b071334e9d93742f789c2c24 -README.zh.md: b1fe2b27426fb38f8798aa54718da3e4253b2887 +README.md: ee84dbcba7493411288c1ce6e1817e5c352a527a +README.zh.md: 46cabe00d2ff967202b984a9daf5d7085bcd71da diff --git a/packages/subagent/subagent/README.md b/packages/subagent/subagent/README.md index 9b87735880..ee84dbcba7 100644 --- a/packages/subagent/subagent/README.md +++ b/packages/subagent/subagent/README.md @@ -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. 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. +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 publishes immutable `agentRouteDefaults` so its provider/model instance defaults become the Consumer's merge baseline before exact-route preflight; `start()` remains authoritative for direct callers and the output cap. 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. diff --git a/packages/subagent/subagent/README.zh.md b/packages/subagent/subagent/README.zh.md index b1fe2b2742..46cabe00d2 100644 --- a/packages/subagent/subagent/README.zh.md +++ b/packages/subagent/subagent/README.zh.md @@ -42,7 +42,7 @@ subagent seam 允许一个 agent(智能体)通过具名提供方把工作委 - `toolFilter`:应用请求的子 agent 工具限制; - `persona`:应用每个子 agent 独立的 persona。 -两个进程内提供方都会声明 `agentOptions`:创建子 agent 时,请求字段会覆盖父级最新记录请求中的提供方、模型与推理强度;首个请求之前回退到其创建选项,并保留其中配置的 token 上限。更换路由但没有显式指定强度时,会清除继承的路由所属强度,使所选模型解析自己的默认值。DSH SDK 也声明该能力,并实现 `resolveAgentOptions()`,在 Consumer 预检确切路由之前填入其实例持有的 provider/model/maxTokens 默认值;直接调用方进入 `start()` 时会应用同一解析。ACP、Codex 与 Claude Code 声明不支持该能力,因此它们的传输会拒绝配置或模型选择的覆盖,而不会静默忽略。 +两个进程内提供方都会声明 `agentOptions`:创建子 agent 时,请求字段会覆盖父级最新记录请求中的提供方、模型与推理强度;首个请求之前回退到其创建选项,并保留其中配置的 token 上限。更换路由但没有显式指定强度时,会清除继承的路由所属强度,使所选模型解析自己的默认值。DSH SDK 也声明该能力,并公开不可变的 `agentRouteDefaults`,使其实例持有的 provider/model 默认值在确切路由预检前成为 Consumer 的合并基线;`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 已能通过工具注册表的全局层解析到它们。 diff --git a/packages/subagent/subagent/src/types.ts b/packages/subagent/subagent/src/types.ts index 23deea7d44..0acdb47438 100644 --- a/packages/subagent/subagent/src/types.ts +++ b/packages/subagent/subagent/src/types.ts @@ -309,15 +309,12 @@ export interface SubagentProvider { */ 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. + * Optional static provider-owned route defaults for one-shot Agent options. + * Consumers merge tool/model overrides over these values before preflight; + * providers whose missing route fields derive from the parent omit it. + * The value is detached immutable data and requires `agentOptions` support. */ - resolveAgentOptions?(requested: AgentOptions | undefined): AgentOptions | undefined + readonly agentRouteDefaults?: Readonly> /** * Establish a ONE-SHOT child and return its handle after publication. * The service has already validated that every requested start-time diff --git a/packages/subagent/tool-subagent/README.i18n.yaml b/packages/subagent/tool-subagent/README.i18n.yaml index b9c132c1a8..6165428a7b 100644 --- a/packages/subagent/tool-subagent/README.i18n.yaml +++ b/packages/subagent/tool-subagent/README.i18n.yaml @@ -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: db84c074f314ef4f61d52587f70bb96b9b46225d -README.zh.md: 6d2cbe5ff79bdec764986eee309f8d90295d61c9 +README.md: efbd70b445b4b203305eb893d9ddf4155ffb1e61 +README.zh.md: 253dfb2df9d343a6ee4d0f07126d11c976d11e55 diff --git a/packages/subagent/tool-subagent/README.md b/packages/subagent/tool-subagent/README.md index db84c074f3..efbd70b445 100644 --- a/packages/subagent/tool-subagent/README.md +++ b/packages/subagent/tool-subagent/README.md @@ -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, 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. +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. Static `provider.agentRouteDefaults`, when present, form the provider/model/reasoning baseline; tool config and model fields overlay it before route-aware effort merging and exact-route preflight. Providers without those defaults 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. 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. | +| `agentOptions` | Configured child LLM `provider`, `model`, adapter-owned `reasoningEffort`, and positive `maxTokens`; requires the subagent provider's `agentOptions` capability. Static provider route defaults, when present, are merged before tool config and model overrides; 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. | diff --git a/packages/subagent/tool-subagent/README.zh.md b/packages/subagent/tool-subagent/README.zh.md index 6d2cbe5ff7..253dfb2df9 100644 --- a/packages/subagent/tool-subagent/README.zh.md +++ b/packages/subagent/tool-subagent/README.zh.md @@ -6,7 +6,7 @@ ## 提供方选择与生命周期 -每个插件实例把一个 subagent 传输 `provider` 绑定到一个 `toolName`;模型不能改变该传输。如需公开另一种传输,请加载另一个名称不同的实例。`enableModelSelection: true`,或 `modelSelectionSettings: true` 时已启用的 Host 偏好,都要求该提供方具备子级 `agentOptions` 能力,并且无需额外路由配置即可公开可选的子 agent LLM `provider`、`model` 与 `reasoning_effort` 字段。调用可以提供完整的提供方/模型对;当配置值、父 Agent 值或提供方持有的默认值能够提供生效路由时,也可以只提供推理强度。模型字段会先覆盖工具 `agentOptions`;实现 `resolveAgentOptions()` 的提供方随后会在实时 adapter 预检确切路由前填入自身缺失的默认值,同一份解析结果再进入 `start()`。没有该钩子的提供方会从父 Agent 最新记录的请求选择中保留兼容的缺失值;首个请求之前回退到其创建选项,并保留其中配置的 `maxTokens`。如果更换提供方或模型但没有指定强度,则清除下层路由所属的强度,使所选模型解析自己的默认值。 +每个插件实例把一个 subagent 传输 `provider` 绑定到一个 `toolName`;模型不能改变该传输。如需公开另一种传输,请加载另一个名称不同的实例。`enableModelSelection: true`,或 `modelSelectionSettings: true` 时已启用的 Host 偏好,都要求该提供方具备子级 `agentOptions` 能力,并且无需额外路由配置即可公开可选的子 agent LLM `provider`、`model` 与 `reasoning_effort` 字段。调用可以提供完整的提供方/模型对;当配置值、父 Agent 值或提供方持有的默认值能够提供生效路由时,也可以只提供推理强度。静态的 `provider.agentRouteDefaults` 在存在时构成 provider/model/reasoning 基线;工具配置与模型字段会在路由相关强度合并和确切路由预检前覆盖它。没有这些默认值的提供方会从父 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 的当前深度,被拒绝时返回出错的工具结果。 | diff --git a/packages/subagent/tool-subagent/src/index.ts b/packages/subagent/tool-subagent/src/index.ts index 76ef165ce3..dfe6e50107 100644 --- a/packages/subagent/tool-subagent/src/index.ts +++ b/packages/subagent/tool-subagent/src/index.ts @@ -365,8 +365,8 @@ 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 + const providerRouteDefaults = subagentProvider.agentRouteDefaults + const selectionDescription = providerRouteDefaults !== undefined ? ' 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 @@ -399,19 +399,19 @@ export function apply(ctx: Context, config: Config): void { ...modelSelectionEnabled ? { provider: { type: 'string' as const, - description: providerOwnsAgentOptionDefaults + description: providerRouteDefaults !== undefined ? '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: providerOwnsAgentOptionDefaults + description: providerRouteDefaults !== undefined ? '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: providerOwnsAgentOptionDefaults + description: providerRouteDefaults !== undefined ? '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.', }, @@ -476,23 +476,32 @@ export function apply(ctx: Context, config: Config): void { const modelRequest = args as DelegationModelRequest const parentOptions = parentAgentOptionsForDelegation(parent) + const requiresRoutePreflight = hasDelegationModelRequest(modelRequest) + || hasConfiguredLlmSelection(config.agentOptions) + const configuredChildAgentOptions = requiresRoutePreflight && providerRouteDefaults !== undefined + ? { ...providerRouteDefaults, ...config.agentOptions } + : config.agentOptions const requestedChildAgentOptions = requestedAgentOptions( parentOptions, - config.agentOptions, + configuredChildAgentOptions, modelRequest, modelSelectionEnabled, ) - 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') } - await preflightChildLlmRoute(llm, parentOptions, childAgentOptions, exec.signal) + await preflightChildLlmRoute( + llm, + parentOptions, + requestedChildAgentOptions, + exec.signal, + providerRouteDefaults === undefined, + ) + if (runtimeCtx.subagents.getProvider(config.provider) !== subagentProvider) { + throw new Error(`subagent provider "${config.provider}" changed while resolving the child LLM route; retry the delegation`) + } } exec.signal.throwIfAborted() const maxDepth = typeof config.maxDepth === 'number' ? config.maxDepth : undefined @@ -500,7 +509,7 @@ export function apply(ctx: Context, config: Config): void { label: args.description, prompt: [{ type: 'text', text: args.prompt }] as ContentBlock[], parent, - ...childAgentOptions !== undefined ? { agentOptions: childAgentOptions } : {}, + ...requestedChildAgentOptions !== undefined ? { agentOptions: requestedChildAgentOptions } : {}, ...config.persona !== undefined ? { persona: config.persona } : {}, ...config.toolFilter !== undefined ? { toolFilter: config.toolFilter } : {}, ...maxDepth !== undefined ? { maxDepth } : {}, diff --git a/packages/subagent/tool-subagent/src/model-selection.ts b/packages/subagent/tool-subagent/src/model-selection.ts index 6b89d92742..906eb496d5 100644 --- a/packages/subagent/tool-subagent/src/model-selection.ts +++ b/packages/subagent/tool-subagent/src/model-selection.ts @@ -89,12 +89,14 @@ export function hasConfiguredLlmSelection(options: AgentOptions | undefined): bo * @param parentOptions - Current parent values whose compatible fields the child inherits. * @param requested - Per-child options after request/config merging. * @param signal - Tool-call cancellation signal. + * @param inheritParentReasoningEffort - Whether an omitted effort may inherit from the parent route. */ export async function preflightChildLlmRoute( llm: LlmRuntime, parentOptions: AgentOptions, requested: AgentOptions | undefined, signal: AbortSignal, + inheritParentReasoningEffort = true, ): Promise { const provider = requested?.provider ?? parentOptions.provider const model = requested?.model ?? parentOptions.model @@ -103,7 +105,7 @@ export async function preflightChildLlmRoute( } const routeChanged = provider !== parentOptions.provider || model !== parentOptions.model const reasoningEffort = requested?.reasoningEffort - ?? (routeChanged ? undefined : parentOptions.reasoningEffort) + ?? (inheritParentReasoningEffort && !routeChanged ? parentOptions.reasoningEffort : undefined) await llm.resolveCallConfig({ provider, model, diff --git a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts index 5d45ea1e5a..95f1c654c6 100644 --- a/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts +++ b/packages/subagent/tool-subagent/tests/tool-subagent.spec.ts @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os' import path from 'node:path' import { Context } from '@deepseek-ai/cordis' import Loader from '@deepseek-ai/cordis-plugin-loader' -import LlmRuntime, { CallId } from '@deepseek-ai/dsh-llm' +import LlmRuntime, { CallId, ReasoningEffortId } from '@deepseek-ai/dsh-llm' import SystemPrompt from '@deepseek-ai/dsh-system-prompt' import ToolRuntime, { TOOL_ABORTED_BEFORE_DISPATCH } from '@deepseek-ai/dsh-tools' import { assembleContextFor, type Agent } from '@deepseek-ai/dsh-agent' @@ -220,8 +220,8 @@ describe('dsh-tool-subagent', () => { expect(text(result)).toContain('abnormally') }) - it('preflights and starts with provider-resolved Agent options', async () => { - let seen: { agentOptions?: { provider?: string; model?: string; maxTokens?: number } } | undefined + it('merges model overrides over provider-owned route defaults before preflight', async () => { + let seen: { agentOptions?: { provider?: string; model?: string; reasoningEffort?: string; maxTokens?: number } } | undefined const ctx = new Context() await ctx.plugin(LlmRuntime) await ctx.plugin(SystemPrompt) @@ -231,7 +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 }), + agentRouteDefaults: { provider: 'alpha', model: 'child-model' }, start: async (request) => { seen = request return { @@ -242,17 +242,73 @@ describe('dsh-tool-subagent', () => { } }, }) - ctx.llm.registerAdapter(['alpha'], new MockAdapter([])) + ctx.llm.registerAdapter(['alpha'], new MockAdapter([], { + efforts: [{ id: ReasoningEffortId('high'), name: 'High' }], + })) await ctx.plugin(tool, { provider: 'capture', - agentOptions: { model: 'child-model' }, + agentOptions: { reasoningEffort: ReasoningEffortId('high'), maxTokens: 321 }, maxDepth: 'provider-managed', }) - await callSubagent(ctx, { description: 'd', prompt: 'p' }) + await callSubagent(ctx, { + description: 'd', + prompt: 'p', + 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 }) + expect(seen?.agentOptions).toEqual({ + provider: 'alpha', + model: 'child-model', + reasoningEffort: 'high', + maxTokens: 321, + }) + }) + + it('does not inherit parent effort for a provider-owned route default', async () => { + let seen: SubagentStartRequest | undefined + const ctx = new Context() + await ctx.plugin(LlmRuntime) + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRuntime) + await ctx.plugin(SubagentRuntime) + ctx.subagents.registerProvider({ + name: 'provider-defaults', + capabilities: { agentOptions: true, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + inheritsParentContext: false, + agentRouteDefaults: { provider: 'alpha', model: 'child-model' }, + start: async (request) => { + seen = request + return { + id: SessionId('provider-default-child'), + localAgent: undefined, + result: Promise.resolve({ output: [{ type: 'text', text: 'ok' }], stopReason: 'completed' as const }), + dispose: async () => {}, + } + }, + }) + ctx.llm.registerAdapter(['alpha'], new MockAdapter([])) + await ctx.plugin(tool, { provider: 'provider-defaults', maxDepth: 'provider-managed' }) + const parent = { + ...fakeAgent('same-route-parent'), + options: { + provider: 'alpha', + model: 'child-model', + reasoningEffort: ReasoningEffortId('high'), + }, + } as Agent + + const result = await callSubagent(ctx, { + description: 'd', + prompt: 'p', + provider: 'alpha', + model: 'child-model', + }, { agent: parent }) + + expect(result.isError).toBe(false) + expect(seen?.agentOptions).toEqual({ provider: 'alpha', model: 'child-model' }) }) it('defaults toolName and omits agentOptions when apply() is called directly (schema bypass)', async () => { @@ -931,6 +987,55 @@ describe('dsh-tool-subagent background mode', () => { expect(ctx.jobs.list(parent)).toEqual([]) }) + it('rejects startup when the provider changes during asynchronous route preflight', async () => { + const ctx = new Context() + await ctx.plugin(LlmRuntime) + await ctx.plugin(SystemPrompt) + await ctx.plugin(ToolRuntime) + await ctx.plugin(SubagentRuntime) + const oldStart = vi.fn(async (): Promise => { throw new Error('old provider must not start') }) + const replacementStart = vi.fn(async (): Promise => { throw new Error('replacement provider must not start') }) + const disposeOld = ctx.subagents.registerProvider({ + name: 'swapped', + capabilities: { agentOptions: true, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + inheritsParentContext: false, + agentRouteDefaults: { provider: 'alpha', model: 'selected-model' }, + start: oldStart, + }) + await ctx.plugin(tool, { provider: 'swapped', maxDepth: 'provider-managed' }) + const adapter = new MockAdapter([]) + let releasePreflight!: () => void + const preflightGate = new Promise((resolve) => { releasePreflight = resolve }) + const resolveModel = vi.spyOn(adapter, 'resolveModel').mockImplementation(async (provider, model) => { + await preflightGate + return { provider, id: model, name: model } + }) + ctx.llm.registerAdapter(['alpha'], adapter) + + const pending = callSubagent(ctx, { + description: 'swapped provider', + prompt: 'do it', + provider: 'alpha', + model: 'selected-model', + }) + await vi.waitFor(() => { expect(resolveModel).toHaveBeenCalledOnce() }) + disposeOld() + ctx.subagents.registerProvider({ + name: 'swapped', + capabilities: { agentOptions: true, outputSchema: false, depthLimit: false, toolFilter: false, persona: false }, + inheritsParentContext: false, + agentRouteDefaults: { provider: 'beta', model: 'replacement-model' }, + start: replacementStart, + }) + releasePreflight() + + const result = await pending + expect(result.isError).toBe(true) + expect(text(result)).toContain('changed while resolving the child LLM route') + expect(oldStart).not.toHaveBeenCalled() + expect(replacementStart).not.toHaveBeenCalled() + }) + it('settles an asynchronous provider-start failure as a failed task', async () => { const ctx = await backgroundSetup({ provider: 'mock' }) const parent = ownerAgent(ctx, 'sess-parent')