Browse Source

feat(subagent): own editable delegation limits in the backend

Dudu-0223 1 week ago
parent
commit
a69bcf3636
34 changed files with 223 additions and 60 deletions
  1. 2 2
      .agents/notes/implemented/feature/2026-09-15-continuable-activation-capacity.i18n.yaml
  2. 2 0
      .agents/notes/implemented/feature/2026-09-15-continuable-activation-capacity.md
  3. 2 0
      .agents/notes/implemented/feature/2026-09-15-continuable-activation-capacity.zh.md
  4. 2 2
      docs/config-catalog.i18n.yaml
  5. 7 4
      docs/config-catalog.md
  6. 7 4
      docs/config-catalog.zh.md
  7. 1 1
      docs/event-producer-consumer.i18n.yaml
  8. 4 4
      docs/event-producer-consumer.md
  9. 2 2
      docs/module-graph.i18n.yaml
  10. 2 1
      docs/module-graph.md
  11. 2 1
      docs/module-graph.zh.md
  12. 2 2
      docs/subsystems/subagent.i18n.yaml
  13. 7 0
      docs/subsystems/subagent.md
  14. 7 0
      docs/subsystems/subagent.zh.md
  15. 7 1
      packages/extensions/tool-cordis/src/api-catalog.ts
  16. 2 2
      packages/subagent/subagent/README.i18n.yaml
  17. 6 0
      packages/subagent/subagent/README.md
  18. 6 0
      packages/subagent/subagent/README.zh.md
  19. 7 2
      packages/subagent/subagent/package.json
  20. 6 8
      packages/subagent/subagent/src/continuation-activation.ts
  21. 1 1
      packages/subagent/subagent/src/continuation.ts
  22. 25 1
      packages/subagent/subagent/src/index.ts
  23. 57 0
      packages/subagent/subagent/tests/continuation.spec.ts
  24. 3 0
      packages/subagent/subagent/tsconfig.json
  25. 2 2
      packages/subagent/tool-subagent/README.i18n.yaml
  26. 2 2
      packages/subagent/tool-subagent/README.md
  27. 2 2
      packages/subagent/tool-subagent/README.zh.md
  28. 7 6
      packages/subagent/tool-subagent/src/index.ts
  29. 28 1
      packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts
  30. 6 6
      packages/subagent/tool-subagent/tests/tool-subagent.spec.ts
  31. 2 1
      packages/test-support/session-snapshot/tests/fixtures/subagent-activation-limit.ts
  32. 3 0
      pnpm-lock.yaml
  33. 1 1
      snapshots/sdk/subagent-activation-limit/cordis.snapshot.yml
  34. 1 1
      snapshots/sdk/subagent-activation-limit/cordis.yml

+ 2 - 2
.agents/notes/implemented/feature/2026-09-15-continuable-activation-capacity.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-09-15-continuable-activation-capacity.md
-2026-09-15-continuable-activation-capacity.md: 71cdf421c09c80f232bcd5ef8bdd6f1dc2523edb
-2026-09-15-continuable-activation-capacity.zh.md: b55b379352f844297eb529bc32db693fd72642a6
+2026-09-15-continuable-activation-capacity.md: b7361f76bd3fd7f5de745596596b3e5e132c1acb
+2026-09-15-continuable-activation-capacity.zh.md: 8d5c306f6c827d761a9e110d8f8c640c83d91a81

+ 2 - 0
.agents/notes/implemented/feature/2026-09-15-continuable-activation-capacity.md

@@ -16,6 +16,8 @@ The Activation registry reserves a unique slot before fresh or cold-resume recon
 
 Pool lookup, admission and release take amortized constant time. A weak root map does not retain dead root Agents; each pool holds only occupied tokens. No Session catalog scan, tree traversal, durable counter, or public capacity-query API is added.
 
+The Host registers the `subagent` settings section over its composition. Each reservation reads the current capacity, so lowering it never evicts resident children or rebuilds their registry. Delegation tools resolve an omitted depth from the same section at each attempt; explicit numeric and provider-managed tool policies retain priority. Keeping counts in the pool and policy in settings avoids a second live counter or a settings-triggered teardown.
+
 ## Alternatives considered
 
 A cumulative child count is a separate cost policy and does not release capacity after useful work finishes. Counting model requests would omit waiting parents, queued inbox work, reconstruction and cleanup, all of which retain resources. Scanning resident and pending maps adds work proportional to unrelated live trees. Separate counters require synchronization with slot ownership.

+ 2 - 0
.agents/notes/implemented/feature/2026-09-15-continuable-activation-capacity.zh.md

@@ -16,6 +16,8 @@ Activation registry 在新建或冷恢复重建首次让出执行前预占唯一
 
 池查找、接纳和释放的摊还时间复杂度均为常数。根代理的弱引用映射不会保留已结束的根 Agent;每个池只持有已占用的 token。不增加 Session 目录扫描、树遍历、持久计数器或公开的容量查询 API。
 
+Host 在组合配置之上注册 `subagent` 设置分节。每次预占读取当前容量,因此调低上限不会驱逐驻留子代理,也不会重建注册表。委派工具在每次尝试时从同一分节解析省略的深度;显式数值和 provider-managed 工具策略保留优先级。池持有计数、设置持有策略,避免增加第二份在线计数或因设置变更而触发拆除。
+
 ## Alternatives considered
 
 累计子代理计数是另一种成本策略,不会在有用工作完成后释放容量。只统计模型请求会遗漏等待后代的父代理、收件箱排队内容、重建和清理,这些阶段都占用资源。扫描驻留和待创建映射会增加与无关存活任务树数量成正比的工作。独立计数器需要与名额所有权同步。

+ 2 - 2
docs/config-catalog.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/config-catalog.md
-config-catalog.md: e93b6df09f865e8800c67faca75e8324e34eca48
-config-catalog.zh.md: ae676b0cc1904727dfbe71c57795bd8d87ff6695
+config-catalog.md: c1632bcaaa173d9b7e06bf282cf2c83c73cb6cb0
+config-catalog.zh.md: d175313d2ba0a71d22e031f63661bac6e8470c3c

+ 7 - 4
docs/config-catalog.md

@@ -2578,10 +2578,12 @@ Source: [`packages/storage/storage-sqlite/src/index.ts:24`](../packages/storage/
 export interface Config {
   /** Maximum live continuable children across one root's tree, excluding the root; defaults to 8. */
   maxActiveSubagents?: number
+  /** Default delegation depth for tools without an explicit limit; defaults to 3. */
+  maxDepth?: number
 }
 ```
 
-Source: [`packages/subagent/subagent/src/index.ts:189`](../packages/subagent/subagent/src/index.ts)
+Source: [`packages/subagent/subagent/src/index.ts:190`](../packages/subagent/subagent/src/index.ts)
 
 <a id="deepseek-aidsh-subagent-acp"></a>
 
@@ -3268,13 +3270,14 @@ export interface Config {
     deny?: string[]
   }
   /**
-   * Maximum child depth: a non-negative safe integer (default `3`; `0` forbids
-   * delegation entirely), or `'provider-managed'` to send no cap. A numeric cap
+   * Maximum child depth: a non-negative safe integer (`0` forbids delegation),
+   * or `'provider-managed'` to send no cap. A numeric cap
    * requires the provider's `depthLimit` capability (mount fails loud
    * otherwise). The provider checks the calling agent's current depth at every
    * start; the tool remains model-visible so runtime policy owns rejection.
    * `'provider-managed'` is for an out-of-process provider whose recursion
-   * budget belongs to the child runtime or its own deployment.
+   * budget belongs to the child runtime or its own deployment. Omission reads
+   * the current Host subagent depth setting (default `3`) at each delegation.
    */
   maxDepth?: number | 'provider-managed'
 }

+ 7 - 4
docs/config-catalog.zh.md

@@ -2580,10 +2580,12 @@ export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
 export interface Config {
   /** Maximum live continuable children across one root's tree, excluding the root; defaults to 8. */
   maxActiveSubagents?: number
+  /** Default delegation depth for tools without an explicit limit; defaults to 3. */
+  maxDepth?: number
 }
 ```
 
-来源: [`packages/subagent/subagent/src/index.ts:189`](../packages/subagent/subagent/src/index.ts)
+来源: [`packages/subagent/subagent/src/index.ts:190`](../packages/subagent/subagent/src/index.ts)
 
 <a id="deepseek-aidsh-subagent-acp"></a>
 
@@ -3270,13 +3272,14 @@ export interface Config {
     deny?: string[]
   }
   /**
-   * Maximum child depth: a non-negative safe integer (default `3`; `0` forbids
-   * delegation entirely), or `'provider-managed'` to send no cap. A numeric cap
+   * Maximum child depth: a non-negative safe integer (`0` forbids delegation),
+   * or `'provider-managed'` to send no cap. A numeric cap
    * requires the provider's `depthLimit` capability (mount fails loud
    * otherwise). The provider checks the calling agent's current depth at every
    * start; the tool remains model-visible so runtime policy owns rejection.
    * `'provider-managed'` is for an out-of-process provider whose recursion
-   * budget belongs to the child runtime or its own deployment.
+   * budget belongs to the child runtime or its own deployment. Omission reads
+   * the current Host subagent depth setting (default `3`) at each delegation.
    */
   maxDepth?: number | 'provider-managed'
 }

+ 1 - 1
docs/event-producer-consumer.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/event-producer-consumer.md
-event-producer-consumer.md: ad9d4e00cb7b86ad2a080b7b602e035d4854b343
+event-producer-consumer.md: 2cfd3c3bcde3d016e9e1c4244d9a48d0aec988d1
 event-producer-consumer.zh.md: 69ca8071bd4574fee9a50799b25c5771cf6644e8

+ 4 - 4
docs/event-producer-consumer.md

@@ -61,10 +61,10 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `settings/document-updated` | `emit` | [`packages/settings/settings/src/types.ts:105`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | `remotes` |
 | `settings/updated` | `emit` | [`packages/settings/settings/src/types.ts:92`](../packages/settings/settings/src/types.ts) | [`settings`](../packages/settings/settings) (`events.dispatch`) | [`settings`](../packages/settings/settings) |
 | `skills/change` | `emit` | [`packages/skill/skill/src/index.ts:296`](../packages/skill/skill/src/index.ts) | [`skill`](../packages/skill/skill) (`events.dispatch`) | - |
-| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:171`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) |
-| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:145`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:151`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
-| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:162`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) |
+| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:172`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), `server`, [`subagent`](../packages/subagent/subagent) |
+| `subagent/provider-added` | `emit` | [`packages/subagent/subagent/src/index.ts:146`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`emit`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `subagent/provider-removed` | `emit` | [`packages/subagent/subagent/src/index.ts:152`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`subagent`](../packages/subagent/subagent), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:163`](../packages/subagent/subagent/src/index.ts) | [`subagent`](../packages/subagent/subagent) (`events.dispatch`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`subagent`](../packages/subagent/subagent) |
 | `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:31`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`waterfall`) | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), `browser-use-runtime`, [`session-reference`](../packages/context/session-reference), [`system-prompt`](../packages/core/system-prompt) |
 | `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:37`](../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../packages/core/system-prompt) (`emit`) | - |
 | `tools/change` | `emit` | [`packages/core/tools/src/index.ts:201`](../packages/core/tools/src/index.ts) | [`agent-presets`](../packages/preset/agent-presets) (`emit`), [`tools`](../packages/core/tools) (`emit`) | `browser-use-runtime`, [`tool-subagent`](../packages/subagent/tool-subagent) |

+ 2 - 2
docs/module-graph.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/module-graph.md
-module-graph.md: 719273c14784fcf5dd98b2fddabfb7666c666139
-module-graph.zh.md: b1098cc39fdc1d44f9d7edc3555d18a2f2c26bc5
+module-graph.md: 2b8f34025fc87eb3611cb7902597afe03ad761aa
+module-graph.zh.md: f8f8f1693a11e9fce21a309205a7dbd445a3fc9b

+ 2 - 1
docs/module-graph.md

@@ -1062,6 +1062,7 @@ flowchart TD
   pkg_subagent --> pkg_session_projection
   pkg_subagent --> pkg_session_projection_cache
   pkg_subagent --> pkg_session_query
+  pkg_subagent --> pkg_settings
   pkg_subagent --> pkg_system_prompt
   pkg_subagent --> pkg_tools
   pkg_subagent --> pkg_typert_protocol
@@ -1542,7 +1543,7 @@ flowchart TD
 | [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | `ssh` | [`ssh`](../packages/ssh/ssh), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local) |
 | [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
-| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time) |
+| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time) |
 | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
 | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) |

+ 2 - 1
docs/module-graph.zh.md

@@ -1064,6 +1064,7 @@ flowchart TD
   pkg_subagent --> pkg_session_projection
   pkg_subagent --> pkg_session_projection_cache
   pkg_subagent --> pkg_session_query
+  pkg_subagent --> pkg_settings
   pkg_subagent --> pkg_system_prompt
   pkg_subagent --> pkg_tools
   pkg_subagent --> pkg_typert_protocol
@@ -1544,7 +1545,7 @@ flowchart TD
 | [`subprocess-ssh`](../packages/ssh/subprocess-ssh) | `ssh` | [`ssh`](../packages/ssh/ssh), [`subprocess`](../packages/subprocess/subprocess), [`subprocess-local`](../packages/subprocess/subprocess-local) |
 | [`agent-loop-testkit`](../packages/test-support/agent-loop-testkit) | `test-support` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) |
 | [`webhook`](../packages/webhook/webhook) | `webhook` | [`agent`](../packages/core/agent), [`agent-default-model`](../packages/core/agent-default-model), [`agent-presets`](../packages/preset/agent-presets), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`session`](../packages/core/session), [`session-title`](../packages/session/session-title), [`workspace`](../packages/workspace/workspace) |
-| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time) |
+| [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`agent-presets`](../packages/preset/agent-presets), [`attachment`](../packages/attachment/attachment), [`invariants`](../packages/runtime-diagnostics/invariants), [`jobs`](../packages/jobs/jobs), [`llm`](../packages/llm/llm), [`permission-presets`](../packages/interaction/permission-presets), [`sandbox`](../packages/sandbox/sandbox), [`sandbox-policy`](../packages/sandbox/sandbox-policy), [`scope`](../packages/core/scope), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-query`](../packages/session-query/session-query), [`settings`](../packages/settings/settings), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`typert-protocol`](../packages/typert/protocol), [`user-approval`](../packages/interaction/user-approval), [`util-time`](../packages/util/time) |
 | [`session-query-sqlite`](../packages/session-query/session-query-sqlite) | `session-query` | [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`session-query`](../packages/session-query/session-query) |
 | [`tool-session-query`](../packages/session-query/tool-session-query) | `session-query` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`session-query`](../packages/session-query/session-query), [`system-prompt`](../packages/core/system-prompt), [`timeout`](../packages/util/timeout), [`tools`](../packages/core/tools) |
 | [`acp`](../packages/acp/acp) | `acp` | [`agent`](../packages/core/agent), [`attachment`](../packages/attachment/attachment), [`llm`](../packages/llm/llm), [`mcp-client`](../packages/mcp/mcp-client), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`token-meter`](../packages/llm/token-meter), [`user-approval`](../packages/interaction/user-approval) |

+ 2 - 2
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: 21c75626158db666fd72bed2742b30e12f4cb719
-subagent.zh.md: ca4bb724b82f4f9e81f1e5b7653950ad8c2d3745
+subagent.md: 74d08b108c8a248873228e6e4c940db90b937b9b
+subagent.zh.md: e7cefff85ac79cb9411ac301e09793336cdb9c2b

+ 7 - 0
docs/subsystems/subagent.md

@@ -494,6 +494,13 @@ Source: [`packages/subagent/tool-subagent/src/model-selection-settings.ts`](../.
 Named provider registry with one-shot runs, durable discovery, and continuable-child operations.
 
 ```ts cordis-catalog
+/**
+ * Resolve a delegation tool's depth policy against the current user setting.
+ * @param configured - Explicit tool limit, or provider-managed for external delegation.
+ * @returns The numeric limit, or undefined when the provider owns depth enforcement.
+ */
+resolveMaxDepth(configured?: number | 'provider-managed'): number | undefined
+
 /**
  * Establish one durable continuable child and deliver its initial prompt.
  * Resolves when the child's inbox accepts that prompt, without waiting for the

+ 7 - 0
docs/subsystems/subagent.zh.md

@@ -498,6 +498,13 @@ Source: [`packages/subagent/tool-subagent/src/model-selection-settings.ts`](../.
 Named provider registry with one-shot runs, durable discovery, and continuable-child operations.
 
 ```ts cordis-catalog
+/**
+ * Resolve a delegation tool's depth policy against the current user setting.
+ * @param configured - Explicit tool limit, or provider-managed for external delegation.
+ * @returns The numeric limit, or undefined when the provider owns depth enforcement.
+ */
+resolveMaxDepth(configured?: number | 'provider-managed'): number | undefined
+
 /**
  * Establish one durable continuable child and deliver its initial prompt.
  * Resolves when the child's inbox accepts that prompt, without waiting for the

+ 7 - 1
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -2460,6 +2460,12 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
     summary: 'Named provider registry with one-shot runs, durable discovery, and continuable-child operations.',
     description: 'Named provider registry with one-shot runs, durable discovery, and continuable-child operations.',
     methods: [
+      {
+        signature: 'resolveMaxDepth(configured?: number | \'provider-managed\'): number | undefined',
+        description: 'Resolve a delegation tool\'s depth policy against the current user setting.',
+        parameters: [{ name: 'configured', description: 'Explicit tool limit, or provider-managed for external delegation.' }],
+        returns: 'The numeric limit, or undefined when the provider owns depth enforcement.',
+      },
       {
         signature: 'async startContinuable(spec: ContinuableStartSpec): Promise<ContinuableStart>',
         description: 'Establish one durable continuable child and deliver its initial prompt. Resolves when the child\'s inbox accepts that prompt, without waiting for the turn to start or for the message to reach the Session log; any earlier failure rejects with no ids and rolls back the child entirely.',
@@ -6203,7 +6209,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'SubagentRuntime',
-    declaration: 'export class SubagentRuntime extends TypertRemoteService {\n    static Config: z<Config>;\n    constructor(ctx: Context, config: Config);\n    async startContinuable(spec: ContinuableStartSpec): Promise<ContinuableStart>;\n    async sendMessage(sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions): Promise<MessageId>;\n    interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void;\n    async drainContinuableDescendants(parents: readonly Agent[]): Promise<void>;\n    async drainContinuableChildren(parent: Agent, childIds: readonly SessionId[]): Promise<void>;\n    listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise<SubagentListEntry[]>;\n    listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<SubagentDescendantListEntry[]>;\n    @Remote(\'list\')\n    async remoteExportList(parentSessionId: SessionId, signal: AbortSignal): Promise<SubagentCatalog>;\n    @Remote(\'prompt\')\n    async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise<SubagentPromptReceipt>;\n    @Remote(\'interruptByParent\')\n    interruptByParent(childSessionId: SessionId, parentSessionId: SessionId, mode: \'continuable\'): SubagentInterruptReceipt;\n    registerProvider(provider: SubagentProvider): () => void;\n    getProvider(name: string): SubagentProvider | undefined;\n    list(): string[];\n    async start(name: string, request: SubagentStartRequest): Promise<SubagentRun>;\n}',
+    declaration: 'export class SubagentRuntime extends TypertRemoteService {\n    static Config: z<Config>;\n    constructor(ctx: Context, config: Config);\n    resolveMaxDepth(configured?: number | \'provider-managed\'): number | undefined;\n    async startContinuable(spec: ContinuableStartSpec): Promise<ContinuableStart>;\n    async sendMessage(sender: Agent, targetId: SessionId, content: ContentBlock[], options: SubagentSendMessageOptions): Promise<MessageId>;\n    interrupt(targetSessionId: SessionId, authority: SubagentInterruptAuthority): void;\n    async drainContinuableDescendants(parents: readonly Agent[]): Promise<void>;\n    async drainContinuableChildren(parent: Agent, childIds: readonly SessionId[]): Promise<void>;\n    listChildren(parentSessionId: SessionId, signal?: AbortSignal): Promise<SubagentListEntry[]>;\n    listDescendants(rootSessionId: SessionId, signal?: AbortSignal): Promise<SubagentDescendantListEntry[]>;\n    @Remote(\'list\')\n    async remoteExportList(parentSessionId: SessionId, signal: AbortSignal): Promise<SubagentCatalog>;\n    @Remote(\'prompt\')\n    async prompt(request: SubagentPromptRequest, signal: AbortSignal): Promise<SubagentPromptReceipt>;\n    @Remote(\'interruptByParent\')\n    interruptByParent(childSessionId: SessionId, parentSessionId: SessionId, mode: \'continuable\'): SubagentInterruptReceipt;\n    registerProvider(provider: SubagentProvider): () => void;\n    getProvider(name: string): SubagentProvider | undefined;\n    list(): string[];\n    async start(name: string, re /* …truncated — full shape in source */',
   },
   {
     name: 'SubagentSendMessageOptions',

+ 2 - 2
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: ea542aba2fa482618d75342941043a4f3b6bd531
-README.zh.md: 57efd34abbc022b5ab3429c7a951bc679d3c1c69
+README.md: a38022d7f6373a7cf40b42cf944449eabc5aef90
+README.zh.md: 452b4648a018f788daf1d881c8ed3e91b77286e7

+ 6 - 0
packages/subagent/subagent/README.md

@@ -42,10 +42,16 @@ Mount the service with a provider and the delegation tool. The provider register
 
 An agent that calls the tool gets the child's final answer as the tool result. Mounting the service alone changes nothing: nothing can delegate until a provider and a tool are composed.
 
+### Delegation settings
+
+The Host exposes delegation defaults in the `subagent` settings section. User values override this plugin's composition; reset removes the user override. `maxDepth` defaults to `3` and supplies the delegation tools' depth when their own configuration omits it. An explicit tool depth, including `provider-managed`, takes precedence. Depth `0` disables delegation through tools inheriting this setting; depth `1` permits direct children only. Changes apply on the next delegation attempt. Direct service callers continue to supply their own optional request depth.
+
 ### Continuable capacity
 
 Set `maxActiveSubagents` on the host `dsh-subagent` plugin to limit live continuable children across each root Agent's entire continuable tree. It defaults to `8` and accepts positive safe integers. The root does not consume a slot; descendants inherit one shared pool. Fresh creation and cold resume reserve before reconstructing the Agent, and cleanup returns the slot after handle disposal. A waiting parent, pending inbox work, and an Activation being stopped still occupy slots. Messages to a resident child reuse its slot. One-shot and external-provider runs are outside this limit; depth remains the delegation tool's separate policy.
 
+The current `maxActiveSubagents` value is sampled before every new or cold-resumed Activation. Raising it admits more children in existing trees; lowering it leaves resident children running and refuses further admissions until usage is below the limit.
+
 At capacity, creation or cold resume rejects with `ACTIVATION_LIMIT_REACHED`: wait for a child to finish or continue using the existing agents. Admission does not queue, because a parent waiting for descendants must not wait for its own occupied slot. Slots are process-local and do not constrain cumulative Session history or token usage.
 
 ### One-shot and continuable children

+ 6 - 0
packages/subagent/subagent/README.zh.md

@@ -42,10 +42,16 @@ kind: "package-reference"
 
 调用该工具的 agent 会把子 agent 的最终答案作为工具结果收到。只挂载服务本身不会改变任何行为:在组合出提供方和工具之前,什么都不能委派。
 
+### 委派设置
+
+Host 在 `subagent` 设置分节中提供委派默认值。用户值覆盖本插件的组合配置;恢复默认会删除用户覆盖。`maxDepth` 默认为 `3`,在委派工具自身未配置深度时提供默认值。工具显式指定的深度(包括 `provider-managed`)优先。深度 `0` 禁止继承此设置的工具委派;深度 `1` 只允许直接子代理。修改在下一次委派时生效。直接调用服务的调用方仍自行提供可选的请求深度。
+
 ### 可续接子代理容量
 
 在 Host 的 `dsh-subagent` 插件上设置 `maxActiveSubagents`,限制每个根 Agent 的整棵可续接子代理树中存活的子代理数。默认值为 `8`,接受正安全整数。根 Agent 不占名额;后代继承同一个共享池。新建和冷恢复在重建 Agent 前预占名额,清理在 handle 释放后归还名额。等待后代的父代理、有待处理收件箱内容的代理以及正在停止的 Activation 仍占名额。向驻留子代理发送消息复用其名额。一次性和外部提供方运行不受此限制;深度仍由委派工具的独立策略决定。
 
+每次新建或冷恢复 Activation 前都会读取当前 `maxActiveSubagents`。调高后已有树可接纳更多子代理;调低后驻留子代理继续运行,使用量降至上限以下前拒绝新接纳。
+
 容量耗尽时,新建或冷恢复以 `ACTIVATION_LIMIT_REACHED` 拒绝:等待子代理完成,或继续使用现有代理。接纳不会排队,避免等待后代的父代理又等待自己占用的名额。名额仅存在于当前进程,不限制累计 Session 历史或 token 用量。
 
 ### 一次性与可继续子级

+ 7 - 2
packages/subagent/subagent/package.json

@@ -80,7 +80,8 @@
     "@deepseek-ai/dsh-typert-protocol": "workspace:^",
     "@deepseek-ai/dsh-user-approval": "workspace:^",
     "@deepseek-ai/dsh-util-time": "workspace:^",
-    "@deepseek-ai/dsh-permission-presets": "workspace:^"
+    "@deepseek-ai/dsh-permission-presets": "workspace:^",
+    "@deepseek-ai/dsh-settings": "workspace:^"
   },
   "peerDependenciesMeta": {
     "@deepseek-ai/dsh-agent-presets": {
@@ -112,6 +113,9 @@
     },
     "@deepseek-ai/dsh-user-approval": {
       "optional": true
+    },
+    "@deepseek-ai/dsh-settings": {
+      "optional": true
     }
   },
   "devDependencies": {
@@ -139,6 +143,7 @@
     "@deepseek-ai/dsh-typert-protocol": "workspace:^",
     "@deepseek-ai/dsh-user-approval": "workspace:^",
     "@deepseek-ai/dsh-util-time": "workspace:^",
-    "@deepseek-ai/dsh-permission-presets": "workspace:^"
+    "@deepseek-ai/dsh-permission-presets": "workspace:^",
+    "@deepseek-ai/dsh-settings": "workspace:^"
   }
 }

+ 6 - 8
packages/subagent/subagent/src/continuation-activation.ts

@@ -41,13 +41,11 @@ import type { ActivationObserver, ActivationTerminal } from './lifecycle.ts'
 class ActivationPool {
   private readonly slots = new Set<symbol>()
 
-  constructor(private readonly capacity: number) {}
-
   /** Reserve before reconstruction; the returned release also tolerates unpublished rollback. */
-  reserve(): () => void {
-    if (this.slots.size >= this.capacity) {
+  reserve(capacity: number): () => void {
+    if (this.slots.size >= capacity) {
       throw new SubagentError(
-        `subagent limit reached (${this.capacity} active children); wait for an existing child to finish `
+        `subagent limit reached (${capacity} active children); wait for an existing child to finish `
         + 'or complete this work with the current agents',
         'ACTIVATION_LIMIT_REACHED',
       )
@@ -207,7 +205,7 @@ export class ContinuableActivationRegistry {
       childId: SessionId,
       parent: Agent,
     ) => ActivationObserver,
-    private readonly maxActiveSubagents: number,
+    private readonly maxActiveSubagents: () => number,
   ) {
     // Ordinary Cordis owner effects unwind in reverse registration order, which
     // cannot express the dynamic child graph. Register the private scope's
@@ -488,7 +486,7 @@ export class ContinuableActivationRegistry {
     inputs.signal.throwIfAborted()
     const lineage = this.liveLineage(inputs.parent)
     const pool = this.resident.get(inputs.parent.id)?.pool ?? this.rootPool(inputs.parent)
-    const releaseSlot = pool.reserve()
+    const releaseSlot = pool.reserve(this.maxActiveSubagents())
     const settled = Promise.withResolvers<void>()
     const materialization: Materialization = {
       lineage,
@@ -607,7 +605,7 @@ export class ContinuableActivationRegistry {
   private rootPool(parent: Agent): ActivationPool {
     let pool = this.rootPools.get(parent)
     if (pool === undefined) {
-      pool = new ActivationPool(this.maxActiveSubagents)
+      pool = new ActivationPool()
       this.rootPools.set(parent, pool)
     }
     return pool

+ 1 - 1
packages/subagent/subagent/src/continuation.ts

@@ -85,7 +85,7 @@ export class SubagentContinuationManager {
   constructor(
     private readonly ctx: Context,
     private readonly host: ContinuationHost,
-    maxActiveSubagents: number,
+    maxActiveSubagents: () => number,
   ) {
     this.activations = new ContinuableActivationRegistry(
       ctx,

+ 25 - 1
packages/subagent/subagent/src/index.ts

@@ -31,6 +31,7 @@
 
 import { Context } from '@deepseek-ai/cordis'
 import z from '@deepseek-ai/schemastery'
+import type {} from '@deepseek-ai/dsh-settings'
 import type {} from '@deepseek-ai/dsh-attachment'
 import { scopeTarget } from '@deepseek-ai/dsh-scope'
 import type { Scoped } from '@deepseek-ai/dsh-scope'
@@ -189,13 +190,17 @@ interface BrowserPromptSource {
 export interface Config {
   /** Maximum live continuable children across one root's tree, excluding the root; defaults to 8. */
   maxActiveSubagents?: number
+  /** Default delegation depth for tools without an explicit limit; defaults to 3. */
+  maxDepth?: number
 }
 
 /** Named provider registry with one-shot runs, durable discovery, and continuable-child operations. */
 export class SubagentRuntime extends TypertRemoteService {
   static Config: z<Config> = z.object({
+    maxDepth: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(3),
     maxActiveSubagents: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER).default(8),
   })
+  private settingsSource: () => Config
   private providers = new Map<string, SubagentProvider>()
   private continuations: SubagentContinuationManager | undefined
   /**
@@ -207,12 +212,21 @@ export class SubagentRuntime extends TypertRemoteService {
 
   constructor(ctx: Context, config: Config) {
     super(ctx, 'subagents')
+    assertSubagentMaxDepth(config.maxDepth)
+    this.settingsSource = () => config
+    ctx.inject(['settings'], (settingsCtx) => {
+      settingsCtx.settings.installSection(ctx, 'subagent', SubagentRuntime.Config, config, {
+        validate: (value) => { assertSubagentMaxDepth(value.maxDepth) },
+        setSource: (source) => { this.settingsSource = source },
+        onChange: () => {},
+      })
+    })
     this.emitLifecycle = createLifecycleEmitter(this.ctx, parent => scopeTarget(this, parent))
     ctx.inject(['agents'], (childCtx: Context) => {
       const manager = new SubagentContinuationManager(childCtx, {
         prepareContinuable: (name, request) => this.prepareContinuable(name, request),
         observeActivation: (provider, childId, parent) => this.observeActivation(provider, childId, parent),
-      }, (config as Required<Config>).maxActiveSubagents)
+      }, () => (this.settingsSource() as Required<Config>).maxActiveSubagents)
       this.continuations = manager
       childCtx.effect(() => () => {
         /* v8 ignore else -- one injected binding owns the slot until its fiber disposes. */
@@ -226,6 +240,16 @@ export class SubagentRuntime extends TypertRemoteService {
     })
   }
 
+  /**
+   * Resolve a delegation tool's depth policy against the current user setting.
+   * @param configured - Explicit tool limit, or provider-managed for external delegation.
+   * @returns The numeric limit, or undefined when the provider owns depth enforcement.
+   */
+  resolveMaxDepth(configured?: number | 'provider-managed'): number | undefined {
+    if (configured === 'provider-managed') return undefined
+    return configured ?? (this.settingsSource() as Required<Config>).maxDepth
+  }
+
   /**
    * Establish one durable continuable child and deliver its initial prompt.
    * Resolves when the child's inbox accepts that prompt, without waiting for the

+ 57 - 0
packages/subagent/subagent/tests/continuation.spec.ts

@@ -3,6 +3,7 @@ import { mkdtempSync, rmSync } from 'node:fs'
 import { tmpdir } from 'node:os'
 import { join } from 'node:path'
 import { Context } from '@deepseek-ai/cordis'
+import { SettingsProvider, type SettingsNamespace } from '@deepseek-ai/dsh-settings'
 import type { Agent } from '@deepseek-ai/dsh-agent'
 import AgentLoop from '@deepseek-ai/dsh-agent-loop'
 import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
@@ -32,6 +33,13 @@ import {
   dropContinuationActivation,
 } from './continuation-internals.ts'
 
+/** Writable settings isolated to one test Context. */
+class MemorySettings extends SettingsProvider {
+  get writable(): boolean { return true }
+  protected load(): Promise<Record<string, unknown>> { return Promise.resolve({}) }
+  protected persist(_ns: SettingsNamespace, _section: Record<string, unknown>): Promise<void> { return Promise.resolve() }
+}
+
 type Script = ConstructorParameters<typeof MockAdapter>[0]
 
 /** One scripted response that may wait on a caller-released gate before streaming. */
@@ -261,6 +269,55 @@ describe('continuable activation capacity', () => {
     }
   })
 
+  it('layers editable depth over composition and removes the section on disposal', async () => {
+    const ctx = new Context()
+    try {
+      await ctx.plugin(MemorySettings)
+      const fiber = await ctx.plugin(SubagentRuntime, { maxDepth: 4 })
+      expect(ctx.subagents.resolveMaxDepth()).toBe(4)
+      await ctx.settings.update('subagent', { maxDepth: 0 })
+      expect(ctx.subagents.resolveMaxDepth()).toBe(0)
+      expect(ctx.subagents.resolveMaxDepth(2)).toBe(2)
+      expect(ctx.subagents.resolveMaxDepth('provider-managed')).toBeUndefined()
+      await expect(ctx.settings.update('subagent', { maxDepth: -0 })).rejects.toThrow()
+      await expect(ctx.settings.update('subagent', { maxDepth: -1 })).rejects.toThrow()
+      await expect(ctx.settings.update('subagent', { maxDepth: 1.5 })).rejects.toThrow()
+      await expect(ctx.settings.update('subagent', { maxActiveSubagents: 0 })).rejects.toThrow()
+      expect(ctx.subagents.resolveMaxDepth()).toBe(0)
+      await ctx.settings.replace('subagent', {})
+      expect(ctx.subagents.resolveMaxDepth()).toBe(4)
+      await fiber.dispose()
+      expect(ctx.settings.describe().some(section => section.ns === 'subagent')).toBe(false)
+    } finally {
+      await ctx.fiber.dispose()
+    }
+  })
+
+  it('applies capacity edits to an existing root without stopping resident children', async () => {
+    const release = Promise.withResolvers<undefined>()
+    const adapter = new GatedAdapter(Array.from({ length: 4 }, () => ({ chunks: textResponse('done'), gate: release.promise })))
+    const { ctx, parent } = await setupWith(adapter, { maxActiveSubagents: 1 })
+    parkParent(ctx, parent)
+    try {
+      await ctx.plugin(MemorySettings)
+      const first = await ctx.subagents.startContinuable(startSpec(parent))
+      await expect(ctx.subagents.startContinuable(startSpec(parent))).rejects.toMatchObject({ code: 'ACTIVATION_LIMIT_REACHED' })
+      await ctx.settings.update('subagent', { maxActiveSubagents: 2 })
+      const second = await ctx.subagents.startContinuable(startSpec(parent))
+      await ctx.settings.update('subagent', { maxActiveSubagents: 1 })
+      expect(ctx.agents.get(first.childId)).toBeDefined()
+      expect(ctx.agents.get(second.childId)).toBeDefined()
+      await expect(ctx.subagents.startContinuable(startSpec(parent))).rejects.toMatchObject({ code: 'ACTIVATION_LIMIT_REACHED' })
+      await ctx.settings.update('subagent', { maxActiveSubagents: 3 })
+      const third = await ctx.subagents.startContinuable(startSpec(parent))
+      release.resolve(undefined)
+      await Promise.all([first, second, third].map(child => waitNoActivation(ctx, child.childId)))
+    } finally {
+      release.resolve(undefined)
+      await ctx.fiber.dispose()
+    }
+  })
+
   it('defaults to eight live children and reuses capacity after settlement', async () => {
     const release = Promise.withResolvers<undefined>()
     const adapter = new GatedAdapter([

+ 3 - 0
packages/subagent/subagent/tsconfig.json

@@ -76,6 +76,9 @@
     },
     {
       "path": "../../util/chunked-list"
+    },
+    {
+      "path": "../../settings/settings"
     }
   ]
 }

+ 2 - 2
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: f2d9eb3a44d84130caca8e29bbe0546e24a76b34
-README.zh.md: 312856983f0e25cd6eb8e508f4c7b9e9db361044
+README.md: 17491098fefea43bd3e9b195ad790a34aa8ef155
+README.zh.md: 5d910784ee88796a66f9c4fefc6e83d62a524ac9

+ 2 - 2
packages/subagent/tool-subagent/README.md

@@ -50,7 +50,7 @@ Load the subagent service, an in-process or remote backend, and this tool; then
 | `agentOptions` | — | Configured child `provider`, `model`, adapter-owned `reasoningEffort`, and positive `maxTokens` defaults; requires provider `agentOptions` support and overlays any provider-owned route defaults |
 | `persona` | — | Per-child persona; requires the provider's `persona` capability |
 | `toolFilter` | — | Per-child global-tool restriction; requires the `toolFilter` capability |
-| `maxDepth` | `3` | Absolute delegation-depth cap (`0` forbids delegation); `'provider-managed'` sends no cap to an out-of-process provider |
+| `maxDepth` | Host setting (`3`) | Absolute delegation-depth cap (`0` forbids delegation); `'provider-managed'` sends no cap to an out-of-process provider |
 
 The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-tool-subagent) is the exhaustive source for every accepted field and its JSDoc.
 
@@ -60,7 +60,7 @@ Under `one-shot` policy, an omitted `run_in_background` waits in the foreground
 
 Under `continuable` policy, an omitted or `true` `run_in_background` starts a durable child and returns `started subagent <childId>` without waiting for a result; the runtime delivers one settlement notice when the child's Activation ends, and the optional `send_message` tool sends it more work. Set `run_in_background: false` to wait for the result in the foreground.
 
-`maxDepth` caps recursion (default `3`; `0` forbids delegation) and requires a provider with the `depthLimit` capability; `'provider-managed'` leaves the budget to an out-of-process provider. `persona` and `toolFilter` configure every child when the provider supports them, and the tool stays visible at the cap — each attempted start checks the calling agent's current depth and rejects with an errored result.
+`maxDepth` caps recursion (`0` forbids delegation); omission reads the current Host `subagent.maxDepth` setting, initially `3`, at each delegation. A numeric depth requires a provider with the `depthLimit` capability; `'provider-managed'` leaves the budget to an out-of-process provider. `persona` and `toolFilter` configure every child when the provider supports them, and the tool stays visible at the cap — each attempted start checks the calling agent's current depth and rejects with an errored result.
 
 ### Selecting a child LLM
 

+ 2 - 2
packages/subagent/tool-subagent/README.zh.md

@@ -50,7 +50,7 @@ kind: "package-reference"
 | `agentOptions` | — | 配置的子级 `provider`、`model`、适配器所有的 `reasoningEffort` 与正整数 `maxTokens` 默认值;要求提供方支持 `agentOptions`,并会覆盖提供方持有的路由默认值 |
 | `persona` | — | 每个子 agent 独立的 persona;要求提供方具备 `persona` 能力 |
 | `toolFilter` | — | 每个子 agent 独立的全局工具限制;要求提供方具备 `toolFilter` 能力 |
-| `maxDepth` | `3` | 绝对委派深度上限(`0` 禁止委派);`'provider-managed'` 不向进程外提供方发送上限 |
+| `maxDepth` | Host 设置(`3`) | 绝对委派深度上限(`0` 禁止委派);`'provider-managed'` 不向进程外提供方发送上限 |
 
 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-tool-subagent)是每个受支持字段及其 JSDoc 的穷尽式真源。
 
@@ -60,7 +60,7 @@ kind: "package-reference"
 
 `continuable` 策略下,省略或为 `true` 的 `run_in_background` 会启动一个持久化子 agent,并返回 `started subagent <childId>`,不等待结果;子 agent 的 Activation 结束时,运行时投递一条结算通知,可选的 `send_message` 工具会向它发送更多工作。把 `run_in_background` 设为 `false` 可在前台等待结果。
 
-`maxDepth` 限制递归深度(默认 `3`;`0` 禁止委派),并要求提供方具备 `depthLimit` 能力;`'provider-managed'` 把预算留给进程外提供方。当提供方支持时,`persona` 与 `toolFilter` 会配置每个子 agent;工具在达到上限时仍然可见——每次尝试启动都会检查调用 agent 的当前深度,被拒绝时返回出错的工具结果。
+`maxDepth` 限制递归深度(`0` 禁止委派);省略时,每次委派读取 Host 当前的 `subagent.maxDepth` 设置,初始值为 `3`。数值深度要求提供方具备 `depthLimit` 能力;`'provider-managed'` 把预算留给进程外提供方。当提供方支持时,`persona` 与 `toolFilter` 会配置每个子 agent;工具在达到上限时仍然可见——每次尝试启动都会检查调用 agent 的当前深度,被拒绝时返回出错的工具结果。
 
 ### 选择子级 LLM
 

+ 7 - 6
packages/subagent/tool-subagent/src/index.ts

@@ -91,13 +91,14 @@ export interface Config {
     deny?: string[]
   }
   /**
-   * Maximum child depth: a non-negative safe integer (default `3`; `0` forbids
-   * delegation entirely), or `'provider-managed'` to send no cap. A numeric cap
+   * Maximum child depth: a non-negative safe integer (`0` forbids delegation),
+   * or `'provider-managed'` to send no cap. A numeric cap
    * requires the provider's `depthLimit` capability (mount fails loud
    * otherwise). The provider checks the calling agent's current depth at every
    * start; the tool remains model-visible so runtime policy owns rejection.
    * `'provider-managed'` is for an out-of-process provider whose recursion
-   * budget belongs to the child runtime or its own deployment.
+   * budget belongs to the child runtime or its own deployment. Omission reads
+   * the current Host subagent depth setting (default `3`) at each delegation.
    */
   maxDepth?: number | 'provider-managed'
 }
@@ -126,7 +127,7 @@ export const Config: z<Config> = z.object({
     allow: z.array(z.string()).default(undefined as unknown as string[]),
     deny: z.array(z.string()).default(undefined as unknown as string[]),
   }).default(undefined as unknown as { allow: string[]; deny: string[] }),
-  maxDepth: z.union([z.natural().max(Number.MAX_SAFE_INTEGER), z.const('provider-managed' as const)]).default(3),
+  maxDepth: z.union([z.natural().max(Number.MAX_SAFE_INTEGER), z.const('provider-managed' as const)]),
 })
 
 /** Render text blocks from the canonical JSON block array without trusting arbitrary values. */
@@ -326,7 +327,7 @@ export function apply(ctx: Context, config: Config, session?: Session): void {
   ctx.sessionProjections.register(subagentModelSelectionProjectionDefinition)
 
   const assertSubagentProviderConfiguration = (subagentProvider: SubagentProvider): void => {
-    if (typeof config.maxDepth === 'number' && !subagentProvider.capabilities.depthLimit) {
+    if (ctx.subagents.resolveMaxDepth(config.maxDepth) !== undefined && !subagentProvider.capabilities.depthLimit) {
       throw new Error(
         `tool-subagent: provider "${subagentProvider.name}" cannot enforce maxDepth (no depthLimit capability) — `
         + 'set maxDepth: \'provider-managed\' to leave the recursion budget to the provider',
@@ -511,7 +512,7 @@ export function apply(ctx: Context, config: Config, session?: Session): void {
             }
           }
           exec.signal.throwIfAborted()
-          const maxDepth = typeof config.maxDepth === 'number' ? config.maxDepth : undefined
+          const maxDepth = runtimeCtx.subagents.resolveMaxDepth(config.maxDepth)
           const request = {
             label: args.description,
             prompt: [{ type: 'text', text: args.prompt }] as ContentBlock[],

+ 28 - 1
packages/subagent/tool-subagent/tests/model-selection-settings.spec.ts

@@ -23,7 +23,7 @@ import {
   subagentModelSelectionPolicy,
   subagentModelSelectionProjectionDefinition,
 } from '../src/model-selection-state.ts'
-import { text } from './harness.ts'
+import { callSubagent, text } from './harness.ts'
 
 const ALLOWED_MODELS = [{ provider: 'alpha', model: 'fast-model' }]
 
@@ -485,3 +485,30 @@ describe('SubagentModelSelectionConfig', () => {
     await ctx.fiber.dispose()
   })
 })
+
+
+it('reads the saved default depth at each delegation without remounting the tool', async () => {
+  const ctx = await boot(false)
+  const depths: Array<number | undefined> = []
+  try {
+    ctx.subagents.registerProvider({
+      name: 'capture-depth',
+      capabilities: { agentOptions: true, outputSchema: true, depthLimit: true, toolFilter: true, persona: true },
+      inheritsParentContext: false,
+      start: async (request) => {
+        depths.push(request.maxDepth)
+        return { id: SessionId(`depth-${depths.length}`), localAgent: undefined,
+          result: Promise.resolve({ output: [], stopReason: 'completed' as const }), dispose: async () => {} }
+      },
+    })
+    await ctx.plugin(tool, { provider: 'capture-depth' })
+    await callSubagent(ctx, { description: 'first', prompt: 'work' })
+    await ctx.settings.update('subagent', { maxDepth: 5 })
+    await callSubagent(ctx, { description: 'second', prompt: 'work' })
+    await ctx.settings.update('subagent', { maxDepth: 0 })
+    await callSubagent(ctx, { description: 'disabled', prompt: 'work' })
+    expect(depths).toEqual([3, 5, 0])
+  } finally {
+    await ctx.fiber.dispose()
+  }
+})

+ 6 - 6
packages/subagent/tool-subagent/tests/tool-subagent.spec.ts

@@ -323,7 +323,7 @@ describe('dsh-tool-subagent', () => {
       },
     })
     // Direct apply with only `provider` — no toolName, no agentOptions.
-    tool.apply(ctx, { provider: 'bare' })
+    tool.apply(ctx, { maxDepth: 'provider-managed', provider: 'bare' })
     await new Promise(r => setTimeout(r, 10))
 
     expect(ctx.tools.schemas().some(s => s.name === 'subagent')).toBe(true)
@@ -1028,7 +1028,7 @@ describe('dsh-tool-subagent background mode', () => {
       inheritsParentContext: false,
       start: async () => { throw new Error('setup failed') },
     })
-    tool.apply(ctx, { provider: 'broken-start', toolName: 'subagent_broken' })
+    tool.apply(ctx, { maxDepth: 'provider-managed', provider: 'broken-start', toolName: 'subagent_broken' })
 
     const started = await ctx.tools.execute({
       signal: testToolSignal,
@@ -1059,7 +1059,7 @@ describe('dsh-tool-subagent background mode', () => {
         request.signal.addEventListener('abort', () => { reject(new Error('startup aborted')) }, { once: true })
       }),
     })
-    tool.apply(ctx, { provider: 'pending-start', toolName: 'subagent_pending' })
+    tool.apply(ctx, { maxDepth: 'provider-managed', provider: 'pending-start', toolName: 'subagent_pending' })
 
     await ctx.tools.execute({
       signal: testToolSignal,
@@ -1101,7 +1101,7 @@ describe('dsh-tool-subagent background mode', () => {
         }, { once: true })
       }),
     })
-    tool.apply(ctx, { provider: 'broken-start-rollback', toolName: 'subagent_broken_rollback' })
+    tool.apply(ctx, { maxDepth: 'provider-managed', provider: 'broken-start-rollback', toolName: 'subagent_broken_rollback' })
 
     await ctx.tools.execute({
       signal: testToolSignal,
@@ -1154,7 +1154,7 @@ describe('dsh-tool-subagent background mode', () => {
       },
     })
     // Direct apply preserves omitted agentOptions instead of applying schema defaults.
-    tool.apply(ctx, { provider: 'hanging', toolName: 'subagent_hang' })
+    tool.apply(ctx, { maxDepth: 'provider-managed', provider: 'hanging', toolName: 'subagent_hang' })
 
     const startOne = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('h1'), name: 'subagent_hang', arguments: { description: 'one', prompt: 'p', run_in_background: true }, agent: parent })
     const startTwo = await ctx.tools.execute({ signal: testToolSignal, callId: ToolCallId('h2'), name: 'subagent_hang', arguments: { description: 'two', prompt: 'p', run_in_background: true }, agent: parent })
@@ -1371,7 +1371,7 @@ describe('background preflight failure (no orphaned child, by construction)', ()
         }
       },
     })
-    tool.apply(ctx, { provider: 'probe', toolName: 'subagent_probe' })
+    tool.apply(ctx, { maxDepth: 'provider-managed', provider: 'probe', toolName: 'subagent_probe' })
 
     const result = await ctx.tools.execute({
       signal: testToolSignal,

+ 2 - 1
packages/test-support/session-snapshot/tests/fixtures/subagent-activation-limit.ts

@@ -2,7 +2,7 @@
 import type { Context } from '@deepseek-ai/cordis'
 
 export const name = 'subagent-activation-limit'
-export const inject = ['agents']
+export const inject = ['agents', 'settings', 'subagents']
 
 /** Order parent admission and child completion without elapsed-time assumptions. */
 export function apply(ctx: Context): void {
@@ -13,6 +13,7 @@ export function apply(ctx: Context): void {
   })
   ctx.on('agent/pre-step', async ({ agent }, next) => {
     if (agent.session.header.parentSession !== undefined) await parentClosed.promise
+    else await ctx.settings.update('subagent', { maxActiveSubagents: 1 })
     return next()
   })
 }

+ 3 - 0
pnpm-lock.yaml

@@ -10373,6 +10373,9 @@ importers:
       '@deepseek-ai/dsh-session-query':
         specifier: workspace:^
         version: link:../../session-query/session-query
+      '@deepseek-ai/dsh-settings':
+        specifier: workspace:^
+        version: link:../../settings/settings
       '@deepseek-ai/dsh-storage':
         specifier: workspace:^
         version: link:../../storage/storage

+ 1 - 1
snapshots/sdk/subagent-activation-limit/cordis.snapshot.yml

@@ -55,4 +55,4 @@
 - id: subagent
   name: '@deepseek-ai/dsh-subagent'
   config:
-    maxActiveSubagents: 1
+    maxActiveSubagents: 8

+ 1 - 1
snapshots/sdk/subagent-activation-limit/cordis.yml

@@ -1,7 +1,7 @@
 - id: subagent
   name: '@deepseek-ai/dsh-subagent'
   config:
-    maxActiveSubagents: 1
+    maxActiveSubagents: 8
 
 - insert:
     - id: subagent-activation-limit